CVE-2026-16372
Overview
Background
- Certificate validity override
- A stored exception in
nsICertOverrideServicethat tells Gecko to trust an otherwise-untrusted TLS certificate for a specific host, port, and origin attributes. - Content process
- The sandboxed, untrusted child process that renders web content and must not be allowed to define security-critical browser state directly.
- `WindowGlobalChild` / `WindowGlobalParent`
- Paired IPC actors representing one window global (document) in the child and parent, giving the parent a trusted, per-document context tied to a real navigation.
- `RememberValidityOverride`
- The
nsICertOverrideServicemethod that permanently or temporarily records a certificate exception for a given host, port, origin attributes, and certificate.
Root Cause Analysis
The vulnerable path was Document::AddCertException running in a content process, which itself gathered the certificate, hostname, port, and OriginAttributes from mFailedChannel and passed all of them to the parent via PContent’s AddCertException message. The parent’s ContentParent::RecvAddCertException then trusted those content-supplied values wholesale and called overrideService->RememberValidityOverride(aHostName, aPort, aOriginAttributes, aCert, aIsTemporary). This violated the invariant that a privileged parent must never let an untrusted content process choose the certificate, host, port, or origin for a security override — the parent performed no independent validation that the values matched the document’s actual failed channel.
The fix removes the ContentParent endpoint entirely and reroutes the call through WindowGlobalChild::SendAddCertException(aIsTemporary), which carries only the aIsTemporary boolean. Because the message now targets the per-document WindowGlobalParent, the parent can derive the cert, host, port, and origin attributes from its own trusted state for that window global rather than from attacker-controlled IPC arguments. This closes the escalation because the content process can no longer forge which certificate is trusted for which origin.
WindowGlobalChild/WindowGlobalParent so the parent sources them from its own trusted per-document context.Attack Path
- Compromise a content process An attacker gains code execution in a sandboxed content process (for example via a separate renderer bug).
- Forge the IPC arguments
The attacker crafts a
SendAddCertExceptionmessage with an arbitraryaCert,aHostName,aPort, andOriginAttributesinstead of the values tied to a real failed channel. - Parent applies the override
ContentParent::RecvAddCertExceptiontrusts the message and callsRememberValidityOverride, recording a certificate exception for the attacker-chosen host and certificate. - Persisted trust bypass The forged override lets a certificate the attacker controls be accepted for a target origin, defeating TLS validation for that host in the privileged parent’s trust store.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/base/Document.cpp |
modified | |
ifdom/ipc/ContentParent.cpp |
modified | |
ParentIdleListenerdom/ipc/ContentParent.h |
modified | |
nsIOriginsListLoadCallbackdom/ipc/ContentParent.h |
modified | |
nsIWidgetdom/ipc/ContentParent.h |
modified | |
nsIX509Certdom/ipc/ContentParent.h |
modified | |
CrashReporterInitArgsdom/ipc/ContentParent.h |
modified |
Files Changed
dom/base/Document.cppdom/ipc/ContentParent.cppdom/ipc/ContentParent.hdom/ipc/PContent.ipdldom/ipc/PWindowGlobal.ipdldom/ipc/WindowGlobalParent.cppdom/ipc/WindowGlobalParent.h
Audit Directions
- Content-to-parent IPC parametersAudit every
Recv*handler that accepts security-relevant identifiers (certificates, hostnames, ports, principals,OriginAttributes) from a content process and confirm the parent derives or validates them against its own trusted state rather than trusting them. - Privileged service calls behind IPCTrace any
do_GetServicecall in a parent-sideRecv*method (such asnsICertOverrideServiceor other trust stores) and verify the arguments cannot be forged by a compromised child. - Actor scoping of sensitive messagesFlag security-sensitive messages routed through the broad
PContentactor and prefer per-document actors likeWindowGlobalParentthat let the parent reconstruct context from a real navigation.
Patch
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index c912e0d200c..5a98f26aa63 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -1523,91 +1523,22 @@ already_AddRefed<mozilla::dom::Promise> Document::AddCertException(
return nullptr;
}
- nsresult rv = NS_OK;
- if (NS_WARN_IF(!mFailedChannel)) {
- promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
- return promise.forget();
- }
-
- nsCOMPtr<nsIURI> failedChannelURI;
- NS_GetFinalChannelURI(mFailedChannel, getter_AddRefs(failedChannelURI));
- if (!failedChannelURI) {
- promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
- return promise.forget();
- }
-
- nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(failedChannelURI);
- if (!innerURI) {
- promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
- return promise.forget();
- }
-
- nsAutoCString host;
- innerURI->GetAsciiHost(host);
- int32_t port;
- innerURI->GetPort(&port);
-
- nsCOMPtr<nsITransportSecurityInfo> tsi;
- rv = mFailedChannel->GetSecurityInfo(getter_AddRefs(tsi));
- if (NS_WARN_IF(NS_FAILED(rv))) {
- promise->MaybeReject(rv);
- return promise.forget();
- }
- if (NS_WARN_IF(!tsi)) {
- promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
- return promise.forget();
- }
-
- nsCOMPtr<nsIX509Cert> cert;
- rv = tsi->GetServerCert(getter_AddRefs(cert));
- if (NS_WARN_IF(NS_FAILED(rv))) {
- promise->MaybeReject(rv);
- return promise.forget();
- }
- if (NS_WARN_IF(!cert)) {
- promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
- return promise.forget();
- }
-
- if (XRE_IsContentProcess()) {
- ContentChild* cc = ContentChild::GetSingleton();
- MOZ_ASSERT(cc);
- OriginAttributes const& attrs = NodePrincipal()->OriginAttributesRef();
- cc->SendAddCertException(cert, host, port, attrs, aIsTemporary)
- ->Then(GetCurrentSerialEventTarget(), __func__,
- [promise](const mozilla::MozPromise<
- nsresult, mozilla::ipc::ResponseRejectReason,
- true>::ResolveOrRejectValue& aValue) {
- if (aValue.IsResolve()) {
- promise->MaybeResolve(aValue.ResolveValue());
- } else {
- promise->MaybeRejectWithUndefined();
- }
- });
- return promise.forget();
- }
-
- if (XRE_IsParentProcess()) {
- nsCOMPtr<nsICertOverrideService> overrideService =
- do_GetService(NS_CERTOVERRIDE_CONTRACTID);
- if (!overrideService) {
- promise->MaybeReject(NS_ERROR_FAILURE);
- return promise.forget();
- }
-
- OriginAttributes const& attrs = NodePrincipal()->OriginAttributesRef();
- rv = overrideService->RememberValidityOverride(host, port, attrs, cert,
- aIsTemporary);
- if (NS_WARN_IF(NS_FAILED(rv))) {
- promise->MaybeReject(rv);
- return promise.forget();
- }
-
- promise->MaybeResolveWithUndefined();
- return promise.forget();
+ WindowGlobalChild* wgc = GetWindowGlobalChild();
+ if (!wgc) {
+ return nullptr;
}
+ wgc->SendAddCertException(aIsTemporary)
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [promise](const mozilla::MozPromise<
+ nsresult, mozilla::ipc::ResponseRejectReason,
+ true>::ResolveOrRejectValue& aValue) {
+ if (aValue.IsResolve()) {
+ promise->MaybeResolve(aValue.ResolveValue());
+ } else {
+ promise->MaybeRejectWithUndefined();
+ }
+ });
- promise->MaybeReject(NS_ERROR_FAILURE);
return promise.forget();
}
diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp
index 7814d1287c2..912176a024b 100644
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -200,7 +200,6 @@
#include "nsIBidiKeyboard.h"
#include "nsIBrowserDOMWindow.h"
#include "nsICaptivePortalService.h"
-#include "nsICertOverrideService.h"
#include "nsIClipboard.h"
#include "nsIContentAnalysis.h"
#include "nsIContentSecurityPolicy.h"
@@ -230,7 +229,6 @@
#include "nsIURL.h"
#include "nsIUserIdleService.h"
#include "nsIWebBrowserChrome.h"
-#include "nsIX509Cert.h"
#include "nsIXULRuntime.h"
#include "nsPIDNSService.h"
#if defined(MOZ_WIDGET_GTK) || defined(XP_WIN)
@@ -6635,22 +6633,6 @@ mozilla::ipc::IPCResult ContentParent::RecvBHRThreadHang(
return IPC_OK();
}
-mozilla::ipc::IPCResult ContentParent::RecvAddCertException(
- nsIX509Cert* aCert, const nsACString& aHostName, int32_t aPort,
- const OriginAttributes& aOriginAttributes, bool aIsTemporary,
- AddCertExceptionResolver&& aResolver) {
- nsCOMPtr<nsICertOverrideService> overrideService =
- do_GetService(NS_CERTOVERRIDE_CONTRACTID);
- if (!overrideService) {
- aResolver(NS_ERROR_FAILURE);
- return IPC_OK();
- }
- nsresult rv = overrideService->RememberValidityOverride(
- aHostName, aPort, aOriginAttributes, aCert, aIsTemporary);
- aResolver(rv);
- return IPC_OK();
-}
-
mozilla::ipc::IPCResult
ContentParent::RecvAutomaticStorageAccessPermissionCanBeGranted(
nsIPrincipal* aPrincipal,
diff --git a/dom/ipc/ContentParent.h b/dom/ipc/ContentParent.h
index 2f395fa0c81..081dd6a892d 100644
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -64,7 +64,6 @@ class nsITimer;
class ParentIdleListener;
class nsIOriginsListLoadCallback;
class nsIWidget;
-class nsIX509Cert;
namespace CrashReporter {
class CrashReporterInitArgs;
@@ -1216,11 +1215,6 @@ class ContentParent final : public PContentParent,
mozilla::ipc::IPCResult RecvBHRThreadHang(const HangDetails& aHangDetails);
- mozilla::ipc::IPCResult RecvAddCertException(
- nsIX509Cert* aCert, const nsACString& aHostName, int32_t aPort,
- const OriginAttributes& aOriginAttributes, bool aIsTemporary,
- AddCertExceptionResolver&& aResolver);
-
mozilla::ipc::IPCResult RecvAutomaticStorageAccessPermissionCanBeGranted(
nsIPrincipal* aPrincipal,
AutomaticStorageAccessPermissionCanBeGrantedResolver&& aResolver);
diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl
index 01e8fb678b3..1dc61db79a7 100644
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -177,7 +177,6 @@ using mozilla::dom::PerformanceTimingData from "mozilla/dom/PerformanceTiming.h"
using mozilla::dom::MaybeFeaturePolicyInfo from "mozilla/dom/FeaturePolicy.h";
using mozilla::dom::Wireframe from "mozilla/dom/DocumentBinding.h";
using mozilla::PerfStats::MetricMask from "mozilla/PerfStats.h";
-[RefCounted] using class nsIX509Cert from "nsIX509Cert.h";
using nsIDNSService::ResolverMode from "nsIDNSService.h";
using mozilla::dom::UserActivation::Modifiers from "mozilla/dom/UserActivation.h";
using mozilla::dom::PrivateAttributionImpressionType from "mozilla/dom/PrivateAttributionIPCUtils.h";
@@ -1637,14 +1636,6 @@ parent:
async BHRThreadHang(HangDetails aHangDetails);
- /*
- * Adds a certificate exception for the given hostname and port.
- */
- async AddCertException(nullable nsIX509Cert aCert, nsCString aHostName,
- int32_t aPort, OriginAttributes aOriginAttributes,
- bool aIsTemporary)
- returns (nsresult success);
-
/*
* Determines whether storage access can be granted automatically by the
* storage access API without showing a user prompt.
diff --git a/dom/ipc/PWindowGlobal.ipdl b/dom/ipc/PWindowGlobal.ipdl
index 0f72c462d1e..92b4530a8cd 100644
--- a/dom/ipc/PWindowGlobal.ipdl
+++ b/dom/ipc/PWindowGlobal.ipdl
@@ -218,6 +218,8 @@ parent:
async Destroy();
+ async AddCertException(bool aTemporary) returns(nsresult success);
+
async ReloadWithHttpsOnlyException();
async GetStorageAccessPermission(bool aIncludeIdentityCredential) returns(uint32_t permission_action);
diff --git a/dom/ipc/WindowGlobalParent.cpp b/dom/ipc/WindowGlobalParent.cpp
index 3a8388b67e2..0aed80a78ce 100644
--- a/dom/ipc/WindowGlobalParent.cpp
+++ b/dom/ipc/WindowGlobalParent.cpp
@@ -69,6 +69,7 @@
#include "nsFrameLoader.h"
#include "nsFrameLoaderOwner.h"
#include "nsIBrowser.h"
+#include "nsICertOverrideService.h"
#include "nsICookieManager.h"
#include "nsICookieService.h"
#include "nsIEffectiveTLDService.h"
@@ -81,6 +82,7 @@
#include "nsITimer.h"
#include "nsIURIMutator.h"
#include "nsIWebProgressListener.h"
+#include "nsIX509Cert.h"
#include "nsIXPConnect.h"
#include "nsIXULRuntime.h"
#include "nsImportModule.h"
@@ -1595,6 +1597,50 @@ mozilla::ipc::IPCResult WindowGlobalParent::RecvSetSiteIntegrityProtected(
return IPC_OK();
}
+nsresult WindowGlobalParent::DoAddCertException(bool aTemporary) {
+ nsCOMPtr<nsIChannel> failedChannel(GetFailedChannel());
+ NS_ENSURE_TRUE(failedChannel, NS_ERROR_NOT_AVAILABLE);
+
+ nsCOMPtr<nsIURI> failedChannelURI;
+ nsresult rv =
+ NS_GetFinalChannelURI(failedChannel, getter_AddRefs(failedChannelURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> innerURI(NS_GetInnermostURI(failedChannelURI));
+ NS_ENSURE_TRUE(innerURI, NS_ERROR_DOM_INVALID_STATE_ERR);
+
+ nsAutoCString host;
+ rv = innerURI->GetAsciiHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t port;
+ rv = innerURI->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsITransportSecurityInfo> failedSecurityInfo;
+ rv = failedChannel->GetSecurityInfo(getter_AddRefs(failedSecurityInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(failedSecurityInfo, NS_ERROR_NOT_AVAILABLE);
+
+ nsCOMPtr<nsIX509Cert> cert;
+ rv = failedSecurityInfo->GetServerCert(getter_AddRefs(cert));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(cert, NS_ERROR_NOT_AVAILABLE);
+
+ nsCOMPtr<nsICertOverrideService> overrideService(
+ do_GetService(NS_CERTOVERRIDE_CONTRACTID));
+ NS_ENSURE_TRUE(overrideService, NS_ERROR_FAILURE);
+
+ return overrideService->RememberValidityOverride(
+ host, port, mDocumentPrincipal->OriginAttributesRef(), cert, aTemporary);
+}
+
+IPCResult WindowGlobalParent::RecvAddCertException(
+ bool aTemporary, AddCertExceptionResolver&& aResolver) {
+ aResolver(DoAddCertException(aTemporary));
+ return IPC_OK();
+}
+
mozilla::ipc::IPCResult WindowGlobalParent::RecvReloadWithHttpsOnlyException() {
nsresult rv;
nsCOMPtr<nsIURI> currentURI = BrowsingContext()->Top()->GetCurrentURI();
diff --git a/dom/ipc/WindowGlobalParent.h b/dom/ipc/WindowGlobalParent.h
index a8169b91fd5..02c0bc7cd97 100644
--- a/dom/ipc/WindowGlobalParent.h
+++ b/dom/ipc/WindowGlobalParent.h
@@ -357,6 +357,10 @@ class WindowGlobalParent final : public WindowContext,
mozilla::ipc::IPCResult RecvSetSiteIntegrityProtected(
NotNull<nsIURI*> aSourceURI, uint64_t aMaxAge);
+ nsresult DoAddCertException(bool aTemporary);
+ mozilla::ipc::IPCResult RecvAddCertException(
+ bool aTemporary, AddCertExceptionResolver&& aResolver);
+
mozilla::ipc::IPCResult RecvReloadWithHttpsOnlyException();
mozilla::ipc::IPCResult RecvGetStorageAccessPermission(