Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionPrivilege escalation in the DOM: Content Processes component
ComponentDOM
Bug ClassLogic Error
Tracker2013800
Fix commit8dfb0fb7ddaa (firefox) +66/-116
CISA KEVNot listed
CreditedSajeeb Lohani
Disclosed2026-07-21

Background

Certificate validity override
A stored exception in nsICertOverrideService that 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 nsICertOverrideService method 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.

Key insight
The single core mistake was letting a content process supply the certificate, host, port, and origin attributes for a validity override that the parent then applied verbatim; the fix strips those parameters from the IPC and moves the message to WindowGlobalChild/WindowGlobalParent so the parent sources them from its own trusted per-document context.

Attack Path

  1. Compromise a content process An attacker gains code execution in a sandboxed content process (for example via a separate renderer bug).
  2. Forge the IPC arguments The attacker crafts a SendAddCertException message with an arbitrary aCert, aHostName, aPort, and OriginAttributes instead of the values tied to a real failed channel.
  3. Parent applies the override ContentParent::RecvAddCertException trusts the message and calls RememberValidityOverride, recording a certificate exception for the attacker-chosen host and certificate.
  4. 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

An attacker with control of a content process can cause the parent process to persist an arbitrary certificate validity override for a host and origin of their choosing, undermining TLS trust decisions that the content process should not be able to influence. This is a privilege escalation from the sandboxed content process into security state managed by the parent, requiring the attacker to already have compromised or be running code in a content process. The result is a moderate-severity trust bypass rather than direct memory corruption.

Changed Functions

FunctionChangeNotes
if
dom/base/Document.cpp
modified
if
dom/ipc/ContentParent.cpp
modified
ParentIdleListener
dom/ipc/ContentParent.h
modified
nsIOriginsListLoadCallback
dom/ipc/ContentParent.h
modified
nsIWidget
dom/ipc/ContentParent.h
modified
nsIX509Cert
dom/ipc/ContentParent.h
modified
CrashReporterInitArgs
dom/ipc/ContentParent.h
modified

Files Changed

  • dom/base/Document.cpp
  • dom/ipc/ContentParent.cpp
  • dom/ipc/ContentParent.h
  • dom/ipc/PContent.ipdl
  • dom/ipc/PWindowGlobal.ipdl
  • dom/ipc/WindowGlobalParent.cpp
  • dom/ipc/WindowGlobalParent.h

Audit Directions

  • Content-to-parent IPC parameters
    Audit 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 IPC
    Trace any do_GetService call in a parent-side Recv* method (such as nsICertOverrideService or other trust stores) and verify the arguments cannot be forged by a compromised child.
  • Actor scoping of sensitive messages
    Flag security-sensitive messages routed through the broad PContent actor and prefer per-document actors like WindowGlobalParent that let the parent reconstruct context from a real navigation.
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(
Loading diff…