Compromised content process can add persistent TLS certificate overrides for arbitrary hostnames via AddCertException IPC
Categories
(Core :: DOM: Content Processes, defect)
Tracking
()
People
(Reporter: mirzashihab2, Unassigned)
Details
(Keywords: reporter-external, Whiteboard: [client-bounty-form])
Attachments
(2 files)
Compromised content process can add persistent TLS certificate overrides for arbitrary hostnames via AddCertException IPC
Summary
The parent-side IPC handler ContentParent::RecvAddCertException accepts a hostname, port, and X.509 certificate from a content process and stores a permanent TLS certificate override, with no validation of any parameter. A compromised content process can silently add overrides for arbitrary domains (e.g., bankofamerica.com), causing Firefox to accept an attacker-controlled certificate for those domains on all future connections. The override persists to disk and survives browser restarts.
This violates the Fission security boundary: the parent process performs a privileged, persistent, security-critical action based solely on unvalidated content-process input.
Affected Component
dom/ipc/ContentParent.cpp, lines 6540-6554 (RecvAddCertException)dom/ipc/PContent.ipdl, line 1609 (AddCertExceptionmessage definition)
Severity
sec-high -- Sandbox escape primitive. Converts a content-process compromise into persistent MITM capability against non-HSTS-preloaded HTTPS sites.
Root Cause
// dom/ipc/ContentParent.cpp:6540-6554
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();
}
Every parameter -- the certificate, hostname, port, origin attributes, and persistence flag -- comes entirely from the content process. The handler performs:
- No
ValidatePrincipal()check (contrast withRecvAutomaticStorageAccessPermissionCanBeGrantedat line 6557 in the same file, which does callValidatePrincipal) - No check that the content process has any relationship to the hostname
- No check that the content process is displaying a certificate error page
- No user gesture or interaction requirement
- No
IPC_FAILon any condition
The IPDL definition (PContent.ipdl:1609) has no process-type annotations or restrictions. Any web content process can send this message.
IPC Serialization Path
The nsIX509Cert parameter is serialized as raw DER bytes over IPC:
- Write (
ipc/glue/TransportSecurityInfoUtils.cpp:45-54): CallsaParam->SerializeToIPC(aWriter)which writes raw DER bytes. - Read (
ipc/glue/TransportSecurityInfoUtils.cpp:56-76): Createsnew nsNSSCertificate()and callsDeserializeFromIPC(aReader). - Deserialize (
security/manager/ssl/nsNSSCertificate.cpp:638-658): Reads ahasCertboolean and then raw DER bytes viaReadParam(aReader, &mDER). No validation of the certificate content.
A compromised content process provides arbitrary DER bytes that become the trusted certificate.
Legitimate Code Path
The intended caller is Document::AddCertException (dom/base/Document.cpp:1646-1661), invoked when a user clicks "Accept the Risk and Continue" on the about:certerror page. The WebIDL method is gated by [Func="Document::CallerIsTrustedAboutCertError"] (dom/webidl/Document.webidl:319), which only exposes the method on about:certerror documents.
However, a compromised content process bypasses the DOM layer entirely and calls ContentChild::SendAddCertException() directly with arbitrary parameters. The parent handler has no independent validation.
Downstream Override Service
nsCertOverrideService::RememberValidityOverride (security/manager/ssl/nsCertOverrideService.cpp:380-410) computes the SHA-256 fingerprint of whatever certificate it receives, stores it keyed by hostname:port, and with aIsTemporary=false writes it to cert_override.txt on disk. The only input validation is that the hostname is non-empty/ASCII and the port is >= -1.
Partial Mitigation: HSTS Preload and Static Pins
At TLS connection time (not at override storage time), OverrideAllowedForHost (SSLServerCertVerification.cpp:309-372) checks whether the target host is HSTS-preloaded or statically pinned. If so, the stored override is not applied.
This protects a subset of sites. Spot-checking against the preload list (nsSTSPreloadList.inc, ~166k entries):
| Site | HSTS Preloaded | Override effective? |
|---|---|---|
| facebook.com | Yes | No |
| twitter.com | Yes | No |
| github.com | Yes | No |
| google.com | No | Yes |
| paypal.com | No | Yes |
| bankofamerica.com | No | Yes |
| chase.com | No | Yes |
| wellsfargo.com | No | Yes |
| amazon.com | No | Yes |
| microsoft.com | No | Yes |
Most banking, healthcare, enterprise, and government sites are not HSTS-preloaded. Dynamic HSTS (via response header) only protects if the user has previously visited the site and the HSTS cache entry hasn't expired. The override is stored regardless -- it silently waits for any non-HSTS connection to the target.
Exploitation Scenario
- Attacker exploits a memory corruption bug to compromise a content process.
- Attacker generates a self-signed certificate for
bankofamerica.com(any DER bytes work). - Attacker calls
SendAddCertException(cert, "bankofamerica.com", 443, {}, false). - Parent stores the override permanently to
cert_override.txt. No prompt, no UI indication. - Attacker (or a network-level collaborator) performs MITM on the victim's connection to
bankofamerica.comusing the matching self-signed certificate. - Firefox silently accepts the attacker's certificate. All credentials, session tokens, and data are captured.
- The override persists across browser restarts. The attacker can repeat for unlimited domains.
Proof of Concept
See attached files:
poc_setup.sh-- Generates a self-signed certificate and starts a local HTTPS server on port 8443.poc_add_override.js-- Browser Console script that adds a cert override forlocalhost:8443by calling the sameRememberValidityOverride()thatRecvAddCertExceptioncalls, then verifies the override was stored.
Steps to reproduce
- Run
bash poc_setup.sh(leave the server running). - Open Firefox, navigate to
https://localhost:8443. Observe: certificate error (SEC_ERROR_UNKNOWN_ISSUER). - Open
about:config, setdevtools.chrome.enabledtotrue. - Open the Browser Console (
Ctrl+Shift+J-- the browser-wide console, not the per-tab web console). - Paste the contents of
poc_add_override.jsand press Enter. - Navigate to
https://localhost:8443again. Observe: no certificate error. The page loads with the self-signed certificate accepted silently. - Restart Firefox. Navigate to
https://localhost:8443again. Observe: still no error. The override persisted to disk.
The PoC runs in the parent process via the Browser Console, calling the identical nsICertOverrideService::RememberValidityOverride() function. The IPC handler adds zero additional validation beyond this call, so the effect is identical to a compromised content process sending SendAddCertException.
Precedent
Bug 1783504 ("Compromised content process can UpdateDocumentURI") is the same vulnerability class: a parent-side IPC handler that performs a privileged action using unvalidated content-process input. That bug was fixed as a security issue.
Suggested Fix
Option A -- Add validation to the handler:
mozilla::ipc::IPCResult ContentParent::RecvAddCertException(
nsIX509Cert* aCert, const nsACString& aHostName, int32_t aPort,
const OriginAttributes& aOriginAttributes, bool aIsTemporary,
AddCertExceptionResolver&& aResolver) {
+ if (!aCert || aHostName.IsEmpty()) {
+ return IPC_FAIL(this, "Invalid cert exception parameters");
+ }
+ // Verify the content process has a BrowsingContext with a document
+ // from this hostname that is in a certificate error state.
+ bool hasMatchingCertError = false;
+ for (const auto& bt : ManagedPBrowserParent()) {
+ // Check managed BrowsingContexts for matching hostname + cert error state
+ }
+ if (!hasMatchingCertError) {
+ return IPC_FAIL(this, "No matching cert error for this host");
+ }
Option B -- Move cert exception handling to the parent process entirely. The about:certerror page could communicate with the parent via a JSActor message that the parent validates against its own knowledge of which tabs have certificate errors, rather than using a general-purpose IPC message with content-controlled parameters.
Affected Versions
- Firefox Nightly, Beta, Release (all current)
- Firefox ESR 128+
- Thunderbird (all versions using the same Gecko engine)
Files
| File | Role |
|---|---|
dom/ipc/ContentParent.cpp:6540-6554 |
Vulnerable IPC handler |
dom/ipc/PContent.ipdl:1609 |
IPDL message definition (no restrictions) |
dom/base/Document.cpp:1646-1661 |
Legitimate caller (content-side) |
dom/webidl/Document.webidl:317-320 |
WebIDL gate (content-side only, irrelevant to compromised renderer) |
ipc/glue/TransportSecurityInfoUtils.cpp:45-76 |
Cert IPC serialization (raw DER) |
security/manager/ssl/nsNSSCertificate.cpp:638-658 |
Cert deserialization (no validation) |
security/manager/ssl/nsCertOverrideService.cpp:380-410 |
Override storage + disk persistence |
security/manager/ssl/SSLServerCertVerification.cpp:309-372 |
HSTS/pin check at connection time (partial mitigation) |
| Reporter | ||
Comment 1•3 months ago
|
||
| Reporter | ||
Comment 2•3 months ago
|
||
PoC: Browser Console script that adds the cert override
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•2 months ago
|
Description
•