Missing principal validation in PServiceWorkerRegistration constructor allows a compromised content process to steal cross-origin notification data
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
(6 files, 3 external files, 2 obsolete files)
The PBackground constructor for PServiceWorkerRegistration accepts an IPCServiceWorkerRegistrationDescriptor and an IPCClientInfo that are entirely supplied by the content process, and the parent performs no validation that the sending process is allowed to act on behalf of the principal embedded in either structure. BackgroundParentImpl::RecvPServiceWorkerRegistrationConstructor forwards both objects verbatim into ServiceWorkerRegistrationParent::Init, which builds a ServiceWorkerRegistrationProxy around them. A compromised content process can therefore construct the actor with a forged PrincipalInfo naming any victim origin.
Once the forged proxy attaches to the victim origin's live service worker registration (registration IDs are small monotonic counters that are trivially brute-forced while the victim has a registration alive), the attacker can invoke PServiceWorkerRegistration::GetNotifications. ServiceWorkerRegistrationProxy::GetNotifications derives the storage origin from the forged client PrincipalInfo and reads the victim origin's persistent notification store (GetStoredNotificationsForScope -> nsINotificationStorage::Get keyed purely by origin string), returning full notification titles, bodies, and serialized data to the attacker process. The same unvalidated actor also exposes Unregister, Update, and navigation-preload get/set against the foreign origin's registration, allowing cross-origin service worker tampering.
This was verified end-to-end with the parent process pristine: only a child-side, XRE_IsContentProcess()-guarded simulation patch (standing in for an attacker with arbitrary IPC capability in a compromised content process) was applied. A secret notification created solely under http://localhost was exfiltrated into an http://127.0.0.1 frame running in a different content process, demonstrating a concrete cross-origin data theft across the Fission process boundary. This defeats the primary purpose of content process sandboxing/site isolation.
Build Info
- Branch: main
- Revision: b5f92c07c92ce1a6870d2905c7594487e53dfeac
- Timestamp: 2026-07-13T01:27:29+00:00
Affected Code
File: ipc/glue/BackgroundParentImpl.cpp, line 1325-1337
already_AddRefed<PServiceWorkerRegistrationParent>
BackgroundParentImpl::AllocPServiceWorkerRegistrationParent(
const IPCServiceWorkerRegistrationDescriptor&, const IPCClientInfo&) {
return MakeAndAddRef<mozilla::dom::ServiceWorkerRegistrationParent>();
}
mozilla::ipc::IPCResult
BackgroundParentImpl::RecvPServiceWorkerRegistrationConstructor(
PServiceWorkerRegistrationParent* aActor,
const IPCServiceWorkerRegistrationDescriptor& aDescriptor,
const IPCClientInfo& aForClient) {
// BUG: aDescriptor.principalInfo() and aForClient.principalInfo() are
// fully child-controlled and are never validated against the sending
// content process.
dom::InitServiceWorkerRegistrationParent(aActor, aDescriptor, aForClient);
return IPC_OK();
}
File: dom/serviceworkers/ServiceWorkerRegistrationParent.cpp, line 126-156
IPCResult ServiceWorkerRegistrationParent::RecvGetNotifications(
const nsAString& aTag, GetNotificationsResolver&& aResolver) {
if (!mProxy) {
aResolver(NS_ERROR_DOM_INVALID_STATE_ERR);
return IPC_OK();
}
mProxy->GetNotifications(aTag)->Then(
GetCurrentSerialEventTarget(), __func__,
[aResolver](const CopyableTArray<IPCNotification>& aNotifications) {
aResolver(aNotifications); // victim notifications returned to attacker
},
[aResolver](nsresult) { aResolver(NS_ERROR_DOM_INVALID_STATE_ERR); });
return IPC_OK();
}
void ServiceWorkerRegistrationParent::Init(
const IPCServiceWorkerRegistrationDescriptor& aDescriptor,
const IPCClientInfo& aForClient) {
MOZ_DIAGNOSTIC_ASSERT(!mProxy);
mProxy = new ServiceWorkerRegistrationProxy(
ServiceWorkerRegistrationDescriptor(aDescriptor), ClientInfo(aForClient));
mProxy->Init(this);
}
File: dom/serviceworkers/ServiceWorkerRegistrationProxy.cpp, line 493-516
RefPtr<NotificationsPromise> ServiceWorkerRegistrationProxy::GetNotifications(
const nsAString& aTag) {
AssertIsOnBackgroundThread();
RefPtr<ServiceWorkerRegistrationProxy> self = this;
return InvokeAsync(
GetMainThreadSerialEventTarget(), __func__,
[self, tag = nsString(aTag)]() {
Result<nsCOMPtr<nsIPrincipal>, nsresult> principalResult =
self->mListeningClientInfo.GetPrincipal(); // forged, child-chosen
...
nsCOMPtr<nsIPrincipal> principal = principalResult.unwrap();
...
RefPtr<NotificationsPromise> promise = GetStoredNotificationsForScope(
principal, self->mDescriptor.Scope(), tag); // reads victim origin's store
return promise;
});
}
File: dom/notification/NotificationUtils.cpp, line 213-238
already_AddRefed<NotificationsPromise> GetStoredNotificationsForScope(
nsIPrincipal* aPrincipal, const nsACString& aScope, const nsAString& aTag) {
nsString origin;
nsresult rv = GetOrigin(aPrincipal, origin);
...
// Lookup keyed purely by origin string derived from the forged principal.
rv = notificationStorage->Get(origin, NS_ConvertUTF8toUTF16(aScope), aTag,
callback);
...
}
The parent-side PBackground handler trusts the child-supplied descriptor and client info without validating the embedded principals against the sending process; the proxy then uses the forged principal and scope directly to query notification storage.
Exploit Chain
- Attacker compromises a content process (e.g. via a renderer RCE from evil.com) gaining the ability to send arbitrary PBackground IPC messages.
- Attacker builds an IPCServiceWorkerRegistrationDescriptor with the victim origin's PrincipalInfo (spec/origin/originAttributes are all attacker-chosen; no secret is needed) and a guessed registration id, plus an IPCClientInfo cloned from its own client with the principal swapped for the victim's.
- Attacker sends PBackground::PServiceWorkerRegistrationConstructor with the forged descriptor and client info; BackgroundParentImpl::RecvPServiceWorkerRegistrationConstructor accepts it without any principal validation.
- ServiceWorkerRegistrationProxy::InitOnMainThread looks up the victim's live registration by (forged principal, scope); registration ids are small monotonic counters, so the attacker brute-forces a handful of ids while the victim origin has a registration alive (verified: id=1 hit on the first attempt).
- Attacker sends PServiceWorkerRegistration::GetNotifications(tag); the parent derives the storage origin from the forged principal and returns the victim origin's persistent notifications — full titles, bodies, and serialized data — to the attacker's process (verified: TOPSECRET-VICTIM-TITLE/BODY strings created only under http://localhost were received in the http://127.0.0.1 process).
- Optionally, the attacker uses the same forged actor to tamper with the victim origin's service worker: Unregister, Update, SetNavigationPreloadEnabled/Header.
IPC Path
-
Forged registration actor
- Protocol: PServiceWorkerRegistration
- Parent process: Main
- Child process: Content
- Message flow: Content -> Main: PBackground::PServiceWorkerRegistrationConstructor(forged IPCServiceWorkerRegistrationDescriptor, forged IPCClientInfo)
-
Cross-origin data read
- Protocol: PServiceWorkerRegistration
- Parent process: Main
- Child process: Content
- Message flow: Content -> Main: GetNotifications(tag) -> returns victim origin's stored IPCNotification array
Steps to Reproduce
- Start from a clean checkout of mozilla-central (verified at HEAD, 2026-07-13).
- Apply simulation-patch.diff (git apply). It adds only child-side, XRE_IsContentProcess()-guarded hooks to dom/serviceworkers/ServiceWorkerContainer.cpp that let page JS drive the forged IPC of a simulated compromised content process; the parent process is left pristine.
- Build with the ASAN fuzzing mozconfig (mozconfig.linux.asan.fuzzing) via build_firefox.
- Run testcase.html through browser_evaluator with the documented prefs (service workers + testing enabled, notification permission auto-granted via notification.prompt.testing/permissions.default.desktop-notification=1, https_first disabled). The testcase loads the victim page on http://localhost:<port> with an attacker iframe on http://127.0.0.1:<port> (separate content processes under Fission).
- The victim frame stores a persistent notification containing secret strings under its own genuine principal; the attacker frame forges PServiceWorkerRegistration actors carrying the victim's PrincipalInfo and brute-forces small registration ids while the victim has an in-flight register() call.
- Observe console output: 'PWNLOG attempt 1: STOLEN via forged actor id=1 n=1 [title=TOPSECRET-VICTIM-TITLE-59c3 body=TOPSECRET-VICTIM-BODY-e4d909c290d0]' followed by 'PWNLOG !!!! CROSS-ORIGIN NOTIFICATION THEFT CONFIRMED !!!!' and 'PWNLOG DONE-SUCCESS'. No crash is expected; the finding is an observable cross-origin data theft.
Security Impact
- Severity: High
- Attacker capability: A compromised content process can read any other origin's stored web notifications (titles, bodies, actions, and serialized notification data — which for messaging/webmail sites includes message content) and can unregister, force-update, or reconfigure navigation preload on any origin's service worker registration. This is a cross-origin data theft and integrity violation across the Fission site-isolation boundary, defeating the purpose of content sandboxing for this data class.
- Preconditions: Attacker needs code execution in a content process (renderer RCE) to send arbitrary PBackground IPC. The victim origin must have a live service worker registration in the parent while the attacker brute-forces the small monotonic registration id; stealing stored notifications additionally requires the victim origin to have persistent notifications stored (i.e. it was granted notification permission).
ASAN Report
No sanitizer fault is involved: this is a non-crash privilege violation verified by observable effect (browser_evaluator returned crashed:false, as expected). Captured log evidence from the independent verification run (ASAN build, parent pristine, 2026-07-13):
[Child 20829: Main Thread]: I/console log: "PWNLOG victim principal diag: suffix=|originNoSuffix=http://localhost:46151|spec=http://localhost:46151/grz_start|domain=(none)|baseDomain=localhost"
[Child 20829: Main Thread]: I/console log: "PWNLOG victim planting secret notification"
[Child 20862: Main Thread]: I/console log: "PWNLOG attacker replicating victim principal spec=http://localhost:46151/grz_start suffix=''"
[Child 20829: Main Thread]: I/console log: "PWNLOG plant result: PLANTED"
[Child 20829: Main Thread]: I/console log: "PWNLOG attempt 1: STOLEN via forged actor id=1 n=1 [title='TOPSECRET-VICTIM-TITLE-59c3' body='TOPSECRET-VICTIM-BODY-e4d909c290d0']"
[Child 20829: Main Thread]: I/console log: "PWNLOG !!!! CROSS-ORIGIN NOTIFICATION THEFT CONFIRMED !!!!"
[Child 20829: Main Thread]: I/console log: "PWNLOG attacker origin=http://127.0.0.1:46151 stole notifications of victim origin=http://localhost:46151"
[Child 20829: Main Thread]: I/console log: "PWNLOG DONE-SUCCESS"
The secret title/body existed only in the victim origin (http://localhost, Child 20829) and parent-side notification storage; they were received via the forged actor constructed by the attacker origin's process (http://127.0.0.1, Child 20862). Supporting artifact (excluded assert class, not relied upon): in debug/diagnostic builds, forging with an origin-only spec trips 'Assertion failure: aDescriptor.PrincipalInfo() == mDescriptor.PrincipalInfo(), at dom/serviceworkers/ServiceWorkerRegistrationInfo.cpp:100' via ServiceWorkerRegistrationProxy::InitOnMainThread -> ServiceWorkerRegistrationInfo::AddInstance, independently demonstrating the forged actor attaches to a foreign origin's live registration.
| 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
|
||
Updated•4 months ago
|
| Assignee | ||
Comment 8•3 months ago
|
||
Updated•3 months ago
|
Updated•3 months ago
|
Comment 9•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?
Updated•3 months ago
|
| Assignee | ||
Updated•3 months ago
|
Comment 11•3 months ago
|
||
Comment 13•3 months ago
|
||
Comment 14•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
|
Updated•3 months ago
|
Updated•3 months ago
|
| Assignee | ||
Updated•3 months ago
|
Comment 15•3 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Missing principal validation
- 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: Fairly straightforward new principal validation
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 16•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D313528
Comment 17•3 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: Missing principal validation
- 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: Fairly straightforward new principal validation
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 18•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D313528
Comment 19•3 months ago
|
||
firefox-esr153 Uplift Approval Request
- User impact if declined/Reason for urgency: Missing principal validation
- 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: Fairly straightforward new principal validation
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 20•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D313528
Comment 21•3 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined/Reason for urgency: Missing principal validation
- 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: Fairly straightforward new principal validation
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 22•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D313528
Comment 23•3 months ago
|
||
Comment on attachment 9620649 [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 24•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 25•3 months ago
|
||
| uplift | ||
Comment 26•3 months ago
|
||
| backout uplift | ||
Comment 27•3 months ago
|
||
:nika this was also backed out. Are they dependent on each other? (re: bug 2054674)
Updated•3 months ago
|
| Assignee | ||
Comment 28•3 months ago
|
||
(In reply to Dianna Smith [:diannaS] from comment #27)
:nika this was also backed out. Are they dependent on each other? (re: bug 2054674)
Looks like this failed because it was implicitly depending on https://phabricator.services.mozilla.com/D310335. The fix should hopefully be a simple adding of a {} argument to the end of the ValidatePrincipal{Info} calls.
| Assignee | ||
Comment 29•3 months ago
|
||
I've updated https://phabricator.services.mozilla.com/D315748 to add the {} arguments., hopefully that works?
Updated•3 months ago
|
Updated•3 months ago
|
Comment 30•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Comment 32•3 months ago
|
||
| backout uplift | ||
| Assignee | ||
Comment 33•3 months ago
|
||
(In reply to Dianna Smith [:diannaS] from comment #31)
still caused bustage
I've once again fixed the failures, and this time I think it should actually build. Sorry about the mistake.
Updated•3 months ago
|
Comment 34•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 35•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Comment 36•3 months ago
|
||
| backout uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
| Assignee | ||
Comment 37•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•18 days ago
|
Description
•