WeakRef.deref() bypasses nukeCCW — allows read/write access and code injection into nuked cross-compartment wrappers
Categories
(Core :: JavaScript Engine, defect, P3)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr140 | --- | unaffected |
| firefox149 | --- | wontfix |
| firefox150 | --- | wontfix |
| firefox151 | --- | fixed |
People
(Reporter: define.ctf, Assigned: jonco)
References
(Blocks 1 open bug, Regressed 1 open bug, Regression)
Details
(Keywords: regression, reporter-external, Whiteboard: [pp3])
Attachments
(3 files, 3 external files)
User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36
Steps to reproduce:
WeakRef.deref() bypasses nukeCCW(), allowing full read/write access and code
injection into cross-compartment objects that should be dead.
Tested on:
- SpiderMonkey Nightly 151.0a1 (BuildID: 20260412075731, changeset 4181bc77e2e9)
- SpiderMonkey Beta 150.0 (BuildID: 20260412090428, changeset 03829fdb62f2)
Steps:
- Create object in separate compartment via newGlobal({newCompartment: true})
- Create WeakRef to the CCW
- Call nukeCCW() on the CCW
- Call weakref.deref() — returns live, fully functional wrapper
Minimal reproduction:
var g = newGlobal({newCompartment: true});
g.eval("var obj = {secret: 'confidential'};");
var ccw = g.obj;
var wr = new WeakRef(ccw);
nukeCCW(ccw);
// Direct access correctly blocked:
try { ccw.secret; } catch(e) { /* "can't access dead object" */ }
// WeakRef BYPASSES the nuke:
wr.deref().secret; // Returns "confidential" — BUG
Run with: ./js --fuzzing-safe poc.js
Note: Bug 1655917 addressed "Nuking a CCW to a WeakRef clears the target
of the WeakRef" but this is the reverse — a WeakRef TO a nuked CCW still
returns a live wrapper. The fix for 1655917 did not cover this case.
Actual results:
WeakRef.deref() returns a fully functional wrapper after nukeCCW().
Through this wrapper, an attacker can:
- READ properties: wr.deref().secret → "confidential"
- WRITE properties: wr.deref().isAdmin = true (visible to other compartment)
- INJECT functions: wr.deref().handler = function(){...}
(executes with other compartment's privileges when called) - OVERRIDE toString/valueOf cross-compartment
- Object.defineProperty works cross-compartment
- 100/100 nuked CCWs remain accessible via WeakRef
- Survives multiple GC cycles
Expected results:
WeakRef.deref() should return undefined for CCWs that have been nuked,
consistent with how direct access throws "can't access dead object".
nukeCCW() should either:
- Clear WeakRef entries pointing to the nuked wrapper, OR
- WeakRef.deref() should check the wrapper's "dead" flag before returning it
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
Source code analysis confirms the bug exists in real browser code paths:
-
NukeRemovedCrossCompartmentWrapper (js/src/proxy/BaseProxyHandler.cpp:409)
calls NotifyGCNukeWrapper which only does RemoveFromGrayList() —
NO WeakRef cleanup anywhere. -
NukeCrossCompartmentWrappers (js/src/proxy/CrossCompartmentWrapper.cpp:402)
the bulk version also has NO WeakRef cleanup. -
NotifyGCNukeWrapper (js/src/gc/Sweeping.cpp:1211) only removes
from gray list — does not touch WeakRef entries. -
WindowDestroyedEvent::Run() (dom/base/WindowDestroyedEvent.cpp:134)
calls NukeCrossCompartmentWrappers when a window is destroyed
(tab close, navigation). This is a REAL BROWSER code path.
Root cause: When a CCW is nuked, the wrapper object is converted to a
DeadObjectProxy via ProxyObject::nuke(). Direct property access checks
for the dead proxy and throws. But WeakRef stores a reference to the
wrapper JSObject itself — and WeakRef.deref() returns it without
checking if it has been nuked/converted to DeadObjectProxy.
nukeAllCCWs() in the shell correctly prevents WeakRef.deref() from
returning the wrapper, but per-wrapper nukeCCW() and the browser's
NukeCrossCompartmentWrappers() do not.
Validation: 19/19 tests passed including cross-compartment read, write,
code injection, toString/valueOf override, prototype pollution, and
Object.defineProperty — all through the bypassed wrapper.
Full validation test suite attached below.
Updated•7 months ago
|
| Assignee | ||
Comment 3•7 months ago
|
||
This is due to bug 1984091 which changed WeakRef and FinalizationRegistry to stop using CCWs internally. This was done to support symbols as weak ref keys, since we don't support CCWs to symbols.
Comment 4•7 months ago
|
||
This is an interesting finding, but wrapper nuking is done to prevent memory leaks, not as a security measure.
Updated•7 months ago
|
Updated•7 months ago
|
| Assignee | ||
Updated•7 months ago
|
| Reporter | ||
Comment 6•7 months ago
|
||
I was able to confirm this behavior in the real Firefox browser
(not just the JS shell).
Steps in Firefox Nightly browser console (F12):
var f = document.createElement("iframe");
f.src = "about:blank";
document.body.appendChild(f);
f.contentWindow.testObj = {secret: 42};
var wr = new WeakRef(f.contentWindow.testObj);
console.log("Before:", wr.deref()?.secret);
f.src = "about:blank";
setTimeout(() => {
var d = wr.deref();
console.log("After:", d ? d.secret : "cleared");
}, 3000);
Result:
Before: 42
After: 42
Expected:
After: cleared (or undefined)
The WeakRef retains access to the cross-window object after
iframe navigation in the real browser, confirming this is not
shell-only behavior.
Updated•7 months ago
|
Updated•7 months ago
|
| Reporter | ||
Comment 7•7 months ago
|
||
Thanks for the triage. I'd like to add some context that may be worth considering before settling on the severity.
I fully accept Comment 4 that wrapper nuking was not introduced as a security feature. My concern is narrower:
regardless of original intent, nukeCCW and NukeCrossCompartmentWrappers are implicitly invoked at several boundaries
where isolation between contexts matters, and consumers downstream appear to assume that "nuked" means "unreachable."
The WeakRef bypass breaks that observable property.
Call sites where the nuke happens at an isolation-relevant boundary (offered as factual references, not as a claim
about design intent):
- nsGlobalWindowInner::FreeInnerObjects — runs on document navigation and calls js::NukeCrossCompartmentWrappers
against the previous inner window. - WindowDestroyedEvent::Run (dom/base/WindowDestroyedEvent.cpp:134) — tab close / navigation.
- xpc::NukeAllWrappersForRealm — invoked during realm teardown including principal changes.
- WebExtension sandbox teardown in js/xpconnect/src/Sandbox.cpp.
- BrowsingContext discard paths under Fission.
Whether or not these were designed as security boundaries, code on the other side of them does behave as if
reachability ends there. The PoC in Comment 6 reproduces in a real browser via the FreeInnerObjects path on iframe src
navigation.
Observable effects through the WeakRef-revived wrapper, after the nuke:
- READ properties cross-compartment
- WRITE properties cross-compartment
- Install callable functions that later execute on the other compartment's side
- Override toString / valueOf / accessors via Object.defineProperty
An inconsistency that may be useful for scoping a fix — the shell's nukeAllCCWs already produces the expected
behavior, while per-wrapper nukeCCW does not:
// nukeAllCCWs clears the WeakRef:
var g1 = newGlobal({newCompartment: true});
g1.eval("var o = {v:1};");
var wr1 = new WeakRef(g1.o);
nukeAllCCWs(g1); gc(); gc();
wr1.deref(); // undefined
// per-wrapper nukeCCW does not:
var g2 = newGlobal({newCompartment: true});
g2.eval("var o = {v:2};");
var ccw = g2.o;
var wr2 = new WeakRef(ccw);
nukeCCW(ccw); gc(); gc();
wr2.deref(); // still live
Possible fix directions, deferring to jonco on what's appropriate:
- Have ProxyObject::nuke() / NotifyGCNukeWrapper clear matching WeakRef target entries, mirroring nukeAllCCWs.
- Or have WeakRef::deref check IsDeadProxyObject(target) before returning it.
Re severity: I'm not pushing for a specific rating, just flagging that the bypass is reachable from ordinary
navigation in real Firefox and yields cross-compartment read/write/inject after the nuke completes — that may be worth
weighing before closing at S3/P3.
| Assignee | ||
Comment 8•7 months ago
|
||
This adds a new header file js/public/friend/Wrapper.h and moves the nuking
APIs there. It also makes some APIs that are not called from the browser
internal and makes ReportAccessDenied (which is) public.
| Assignee | ||
Comment 9•7 months ago
|
||
JS WeakRefs are not implemented in terms of CCWs so we have to also 'nuke' any
edges from these to their targets. The patch works in the same was as the
previous patch for CC integration.
| Assignee | ||
Comment 10•7 months ago
|
||
Comment 11•7 months ago
|
||
I'm going to go ahead and unhide this. Any information that gets deleted by this could have easily been copied before the nuking happened. We also only do it for references from privileged code because we can't make it visible to web content.
Updated•7 months ago
|
Updated•7 months ago
|
| Reporter | ||
Comment 12•7 months ago
|
||
Thanks for the fix and for clarifying the threat model. Could you let me know whether this qualifies for the Client
Bug Bounty program given the [pp3] marking, or if unhiding places it outside bounty scope? Happy to defer to the
bounty committee either way.
Comment 13•7 months ago
|
||
If it is not a security bug, then it does not qualify for the security bug bounty program. It'll likely get marked as sec-bounty- in the next bounty meeting this week or maybe next week if the bug isn't closed until then.
Thanks again for the report, as it is definitely a bug. A subtle issue and I could see how it looked like it could be a security issue.
Comment 14•7 months ago
|
||
Set release status flags based on info from the regressing bug 1984091
| Reporter | ||
Comment 15•7 months ago
|
||
Understood, thanks for the quick and clear response. Glad the report was useful regardless.
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 16•7 months ago
|
||
Comment 17•7 months ago
|
||
| bugherder | ||
https://hg.mozilla.org/mozilla-central/rev/528edc61ccab
https://hg.mozilla.org/mozilla-central/rev/e769a5b0924c
https://hg.mozilla.org/mozilla-central/rev/b8a5d25bdf0c
| Reporter | ||
Comment 18•7 months ago
|
||
there is CVE?
Comment 19•7 months ago
|
||
This is not a security issue, so we won't issue a CVE.
| Reporter | ||
Comment 20•7 months ago
|
||
thank you so much
Updated•7 months ago
|
| Reporter | ||
Comment 21•5 months ago
|
||
There is bounty
Description
•