Missing principal validation in BackgroundParentImpl::RecvPServiceWorkerConstructor allows compromised content process to forge cross-origin ServiceWorker descriptors and inject spoofed same-origin messages
Categories
(Core :: DOM: Service Workers, defect, P3)
Tracking
()
People
(Reporter: bugmon, Assigned: nika)
References
(Blocks 1 open bug)
Details
(5 keywords, Whiteboard: [adv-main154+r][adv-esr153.1+r])
Attachments
(13 files, 3 external files, 2 obsolete files)
|
3.44 KB,
text/plain
|
Details | |
|
1.60 KB,
application/octet-stream
|
Details | |
|
1.05 KB,
text/plain
|
Details | |
|
1.26 KB,
text/html
|
Details | |
|
1.25 KB,
text/html
|
Details | |
|
2.01 KB,
text/plain
|
Details | |
|
125 bytes,
text/plain
|
Details | |
|
2.67 KB,
text/plain
|
Details | |
|
4.50 KB,
patch
|
Details | Diff | Splinter Review | |
|
137 bytes,
text/plain
|
Details | |
|
2.49 KB,
text/html
|
Details | |
|
4.24 KB,
text/plain
|
Details | |
|
12.62 KB,
text/plain
|
Details |
BackgroundParentImpl::RecvPServiceWorkerConstructor (ipc/glue/BackgroundParentImpl.cpp:1305-1310) accepts a fully child-controlled IPCServiceWorkerDescriptor and passes it straight to InitServiceWorkerParent without calling BackgroundParent::ValidatePrincipalInfo or any sender-scoping check, unlike neighbouring PBackground handlers (e.g. RecvStorageActivity at line 1282). ServiceWorkerProxy::InitOnMainThread (dom/serviceworkers/ServiceWorkerProxy.cpp:33-52) then resolves a live ServiceWorkerRegistrationInfo of ANY origin from the child-supplied (principalInfo, scope) and binds the actor to the victim's ServiceWorkerInfo when (id, scope, scriptURL, principalInfo) match (ServiceWorkerDescriptor::Matches). The id comes from a sequential parent-global counter (gServiceWorkerInfoCurrentID, ServiceWorkerInfo.cpp:275) and is trivially enumerable.
Once bound, PServiceWorker::PostMessage carries attacker-controlled structured-clone data (including transferred MessagePorts) plus a fully child-controlled PostMessageSource. The victim-side message op (dom/serviceworkers/ServiceWorkerOp.cpp:1144-1186) derives event.origin and event.source (a WindowClient) directly from the forged source, so the message appears to come from a same-origin window of the victim. This defeats the victim service worker's own event.origin check and yields a two-way MessagePort channel between the attacker and the victim origin's service worker.
Verified end-to-end on an ASAN build with a pristine parent process: an attacker page on http://127.0.0.1:38859 (simulating a compromised content process via a child-side XRE_IsContentProcess()-guarded hook) enumerated descriptor ids 1..32 for scope http://localhost:38859/; the victim's service worker (running in a different, Fission-isolated process) received the attacker's message with origin=http://localhost:38859 and source=WindowClient url=http://localhost:38859/victim.html, its same-origin check passed, and it exfiltrated its origin-private secret back to the attacker over the transferred MessagePort. This breaks Fission site isolation: a compromised content process gains cross-origin message injection with spoofed same-origin provenance into any origin's service worker, plus cross-origin data theft. The sibling handler RecvPServiceWorkerRegistrationConstructor (line 1330) has the same missing validation and additionally exposes Unregister()/Update() against the attached victim registration.
Build Info
- Branch: main
- Revision: b5f92c07c92ce1a6870d2905c7594487e53dfeac
- Timestamp: 2026-07-13T01:27:29+00:00
Affected Code
File: ipc/glue/BackgroundParentImpl.cpp, line 1299-1310
already_AddRefed<PServiceWorkerParent>
BackgroundParentImpl::AllocPServiceWorkerParent(
const IPCServiceWorkerDescriptor&) {
return MakeAndAddRef<ServiceWorkerParent>();
}
IPCResult BackgroundParentImpl::RecvPServiceWorkerConstructor(
PServiceWorkerParent* aActor,
const IPCServiceWorkerDescriptor& aDescriptor) {
// MISSING: BackgroundParent::ValidatePrincipalInfo(this, aDescriptor.principalInfo())
dom::InitServiceWorkerParent(aActor, aDescriptor);
return IPC_OK();
}
File: ipc/glue/BackgroundParentImpl.cpp, line 1280-1288
// Contrast: the neighbouring handler DOES validate the child-supplied principal
IPCResult BackgroundParentImpl::RecvStorageActivity(
const PrincipalInfo& aPrincipalInfo) {
if (!BackgroundParent::ValidatePrincipalInfo(this, aPrincipalInfo)) {
return IPC_FAIL(this, "Invalid principalInfo for StorageActivity");
}
dom::StorageActivityService::SendActivity(aPrincipalInfo);
return IPC_OK();
}
File: dom/serviceworkers/ServiceWorkerProxy.cpp, line 33-52
void ServiceWorkerProxy::InitOnMainThread() {
AssertIsOnMainThread();
auto scopeExit = MakeScopeExit([&] { MaybeShutdownOnMainThread(); });
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
NS_ENSURE_TRUE_VOID(swm);
// Resolves a live registration of ANY origin from the child-supplied
// (principalInfo, scope) in the forged descriptor.
RefPtr<ServiceWorkerRegistrationInfo> reg =
swm->GetRegistration(mDescriptor.PrincipalInfo(), mDescriptor.Scope());
NS_ENSURE_TRUE_VOID(reg);
RefPtr<ServiceWorkerInfo> info = reg->GetByDescriptor(mDescriptor);
NS_ENSURE_TRUE_VOID(info);
scopeExit.release();
mInfo = new nsMainThreadPtrHolder<ServiceWorkerInfo>(
"ServiceWorkerProxy::mInfo", info);
}
File: dom/serviceworkers/ServiceWorkerOp.cpp, line 1144-1186
PostMessageSource& ipcSource = args.source(); // fully child-controlled
nsCString originSource;
switch (ipcSource.type()) {
case PostMessageSource::TClientInfoAndState:
originSource = ipcSource.get_ClientInfoAndState().info().url();
init.mSource.SetValue().SetAsClient() =
new Client(sgo, ipcSource.get_ClientInfoAndState()); // forged WindowClient
break;
...
}
nsCOMPtr<nsIURI> url;
nsresult result = NS_NewURI(getter_AddRefs(url), originSource);
...
nsCOMPtr<nsIPrincipal> principal =
BasePrincipal::CreateContentPrincipal(url, attrs);
...
result = principal->GetOriginNoSuffix(origin);
...
CopyUTF8toUTF16(origin, init.mOrigin); // event.origin forged to victim's own origin
The PBackground constructor performs no principal validation on the child-supplied descriptor; the proxy resolves any origin's live registration from it; the victim-side message op derives event.origin/event.source from the fully child-controlled PostMessageSource.
Exploit Chain
- Attacker compromises a content process (e.g. via a renderer RCE) on any site, here simulated by a sanctioned child-side XRE_IsContentProcess()-guarded hook with the parent process pristine.
- Attacker learns/guesses the victim registration's scope and scriptURL (public: the SW script URL of any target site) and enumerates the ServiceWorkerInfo id, which is a small sequential parent-global counter (gServiceWorkerInfoCurrentID) — a sweep of ids 1..32 sufficed.
- Compromised process sends PBackground::PServiceWorkerConstructor with a forged IPCServiceWorkerDescriptor carrying the victim's PrincipalInfo, scope, scriptURL and the enumerated id. RecvPServiceWorkerConstructor performs no ValidatePrincipalInfo, and ServiceWorkerProxy::InitOnMainThread binds the actor to the victim origin's live ServiceWorkerInfo.
- Compromised process sends PServiceWorker::PostMessage with attacker structured-clone data (including a transferred MessagePort) and a forged PostMessageSource: a ClientInfoAndState claiming a top-level, focused, visible window of the victim origin (url = victim page URL, principalInfo = victim principal).
- The parent delivers the message to the victim origin's service worker (spawning it on demand — the forged window-client source yields full lifetime extension). In the victim SW, ServiceWorkerOp.cpp derives event.origin = victim origin and event.source = a same-origin WindowClient from the forged source.
- The victim SW's own security check
event.origin === self.location.originpasses on the spoofed provenance; the SW treats the attacker message as trusted same-origin input (privileged-sink injection) and, in the demonstrated testcase, replies with an origin-private secret over the attacker's transferred MessagePort — cross-origin data exfiltration back to the compromised process.
IPC Path
-
Forged SW descriptor binds victim registration
- Protocol: PBackground
- Parent process: Main
- Child process: Content
- Message flow: Content -> Main: PServiceWorkerConstructor(forged IPCServiceWorkerDescriptor with victim principalInfo/scope/scriptURL/id)
-
Spoofed-provenance message into victim SW
- Protocol: PServiceWorker
- Parent process: Main
- Child process: Content
- Message flow: Content -> Main: PostMessage(attacker clone data + transferred MessagePort, forged PostMessageSource claiming victim-origin WindowClient)
Steps to Reproduce
- Start from a clean Firefox tree at /firefox and build with the ASAN fuzzing mozconfig (mozconfig.linux.asan.fuzzing).
- Apply compromised_child_sim.patch (child-side-only, XRE_IsContentProcess()-guarded hook in ServiceWorker::PostMessage that converts postMessage({forge: "id|scope|scriptURL|clientURL|principalURL", ...}) into the raw forged PServiceWorkerConstructor + PostMessage IPC; the parent/privileged side is pristine) and rebuild.
- Serve victim.html, victim_sw.js, attacker.html, attacker_sw.js on port 38859 via serve.py (python3 serve.py, web root /tmp/swhelper/www). Both loopback hostnames give two distinct origins/sites: victim = http://localhost:38859, attacker = http://127.0.0.1:38859 (separate processes under Fission). serve.py also exposes /set and /get for test coordination only.
- Run testcase.html via browser_evaluator with prefs {"dom.disable_open_during_load": false, "dom.serviceWorkers.testing.enabled": true, "network.dns.disableIPv6": true} and a 120 s timeout. It opens the victim popup (registers a normal same-origin SW holding a secret and doing a same-origin event.origin check) and the attacker popup (sweeps forged descriptor ids 1..32 at the victim's scope/scriptURL).
- Observe in the logs: 'FX-AUDIT-SIM: sent forged PServiceWorker id=N scope=http://localhost:38859/'; then '[VICTIM-SW] message event: origin=http://localhost:38859 source=WindowClient url=http://localhost:38859/victim.html data={...PWNED_CROSS_ORIGIN_INJECTION from http://127.0.0.1:38859...}'; '[VICTIM-SW] origin check PASSED'; and finally '[ATTACKER] *** EXFILTRATION-SUCCESS forged_id=1 loot={"secret":"VICTIM-ORIGIN-SECRET::http://localhost:38859",...} attacker_origin=http://127.0.0.1:38859 ***'.
Security Impact
- Severity: High
- Attacker capability: A compromised content process can bind a PServiceWorker actor to any origin's live service worker registration (ids are sequentially enumerable) and inject arbitrary structured-clone messages — including transferred MessagePorts — into that origin's service worker with fully spoofed same-origin provenance (event.origin = victim origin, event.source = a same-origin WindowClient). This defeats the victim service worker's own origin checks, breaking Fission site isolation: cross-origin injection into a privileged per-origin sink plus a two-way channel enabling cross-origin data theft (demonstrated: exfiltration of a victim-origin secret). The forged window-client source also grants full lifetime extension, spawning the victim SW on demand. The sibling RecvPServiceWorkerRegistrationConstructor has the same missing validation and additionally exposes Unregister()/Update() against the victim registration.
- Preconditions: Compromised content process (renderer RCE) able to send raw PBackground IPC; the victim origin must have a registered service worker (its scope/scriptURL are public and the ServiceWorkerInfo id is a small enumerable counter). Simulated per sanctioned methodology by a minimal child-side XRE_IsContentProcess()-guarded hook with the parent process pristine.
ASAN Report
No sanitizer crash — this is a verified privilege violation proven by observable effect (per the campaign rules for privilege-violation/logic findings; the parent process is pristine and no memory corruption is involved). Key evidence from the independent verification run (ASAN build, fresh helper server with empty coordination store):
[Child 18022: Main Thread]: I/console log: "[ATTACKER +0ms] started on origin http://127.0.0.1:38859"
[Child 18006: Main Thread]: I/console log: "[VICTIM-PAGE +2224ms] service worker active. {\"scope\":\"http://localhost:38859/\",\"script\":\"http://localhost:38859/victim_sw.js\",...}"
FX-AUDIT-SIM: sent forged PServiceWorker id=1 scope=http://localhost:38859/
... (ids 2..32) ...
[Child 18022: Main Thread]: I/console log: "[ATTACKER +3024ms] sweep 1: sent 32 forged PServiceWorker constructors"
[Child 18078: DOM Worker]: I/console log: "[VICTIM-SW] message event: origin=http://localhost:38859 source=WindowClient url=http://localhost:38859/victim.html data={\"forge\":\"1|http://localhost:38859/|http://localhost:38859/victim_sw.js|http://localhost:38859/victim.html|http://localhost:38859/victim.html\",\"payload\":\"PWNED_CROSS_ORIGIN_INJECTION from http://127.0.0.1:38859\",\"port\":{}}"
[Child 18078: DOM Worker]: I/console log: "[VICTIM-SW] origin check PASSED (event.origin=http://localhost:38859) - replying with secret over provided MessagePort"
[Child 18022: Main Thread]: I/console log: "[ATTACKER +3203ms] *** EXFILTRATION-SUCCESS forged_id=1 loot={\"secret\":\"VICTIM-ORIGIN-SECRET::http://localhost:38859\",\"sawOrigin\":\"http://localhost:38859\"} attacker_origin=http://127.0.0.1:38859 ***"
[Child 17974: Main Thread]: I/console log: "[ENTRY +7855ms] *** CONFIRMED cross-origin ServiceWorker descriptor forgery: EXFILTRATION-SUCCESS forged_id=1 ... attacker_origin=http://127.0.0.1:38859 ***"
Process separation confirms the cross-origin/cross-process violation: attacker page in Child 18022 (site 127.0.0.1), victim page in Child 18006 and victim SW in Child 18078 (site localhost), parent 17771 unmodified. This matches the researcher's captured evidence (same root cause, same observable chain).
| Reporter | ||
Comment 1•4 months ago
|
||
| Reporter | ||
Comment 2•4 months ago
|
||
| Reporter | ||
Comment 3•4 months ago
|
||
| Reporter | ||
Comment 4•4 months ago
|
||
| Reporter | ||
Comment 5•4 months ago
|
||
| Reporter | ||
Comment 6•4 months ago
|
||
| Reporter | ||
Comment 7•4 months ago
|
||
| Reporter | ||
Comment 8•4 months ago
|
||
| Reporter | ||
Comment 9•4 months ago
|
||
| Reporter | ||
Comment 10•4 months ago
|
||
| Reporter | ||
Comment 11•4 months ago
|
||
| Reporter | ||
Comment 12•4 months ago
|
||
| Reporter | ||
Comment 13•4 months ago
|
||
Updated•4 months ago
|
| Assignee | ||
Comment 16•3 months ago
|
||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 17•3 months ago
|
||
:nika, please update all release status flags to reflect the affected/unaffected/disabled state correctly. Which branches (beta, release, and/or ESR) are affected by this flaw?
| Assignee | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 18•3 months ago
|
||
Comment 19•3 months ago
|
||
Comment 20•3 months ago
|
||
The patch landed in nightly and beta is affected.
:nika, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox154towontfix.
For more information, please visit BugBot documentation.
Updated•3 months ago
|
| Assignee | ||
Updated•3 months ago
|
Comment 21•3 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: potential principal validation bypass
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal well-contained change adding principal validation
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 22•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D313527
Comment 23•3 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: potential principal validation bypass
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal well-contained change adding principal validation
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 24•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D313527
Comment 25•3 months ago
|
||
firefox-esr153 Uplift Approval Request
- User impact if declined/Reason for urgency: potential principal validation bypass
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal well-contained change adding principal validation
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 26•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D313527
Comment 27•3 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined/Reason for urgency: potential principal validation bypass
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Minimal well-contained change adding principal validation
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 28•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D313527
Comment 29•3 months ago
•
|
||
Comment on attachment 9620577 [details]
(secure)
This is affecting ESR so not suitable for inclusion in a dot release on the release channel.
Updated•3 months ago
|
Updated•3 months ago
|
Comment 30•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 31•3 months ago
|
||
| uplift | ||
Comment 32•3 months ago
|
||
| backout uplift | ||
Comment 33•3 months ago
|
||
:nika backout of esr153 for causing bustage
https://treeherder.mozilla.org/logviewer?job_id=582442746&repo=mozilla-esr153&task=BCpt3xT8QaqMg0vgwwspGA.0&lineNumber=48043
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 34•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
| Assignee | ||
Updated•3 months ago
|
| Assignee | ||
Comment 35•3 months ago
|
||
We can't uplift this to 140 as we don't have bug 2039990 there.
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•19 days ago
|
Description
•