Closed Bug 2054667 Opened 4 months ago Closed 3 months ago

Missing principal validation in PServiceWorkerRegistration constructor allows a compromised content process to steal cross-origin notification data

Categories

(Core :: DOM: Service Workers, defect, P3)

defect

Tracking

()

RESOLVED FIXED
155 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 --- wontfix
firefox-esr153 154+ fixed
firefox153 --- wontfix
firefox154 + fixed
firefox155 + fixed

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

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

  1. Attacker compromises a content process (e.g. via a renderer RCE from evil.com) gaining the ability to send arbitrary PBackground IPC messages.
  2. 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.
  3. Attacker sends PBackground::PServiceWorkerRegistrationConstructor with the forged descriptor and client info; BackgroundParentImpl::RecvPServiceWorkerRegistrationConstructor accepts it without any principal validation.
  4. 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).
  5. 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).
  6. 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

  1. Start from a clean checkout of mozilla-central (verified at HEAD, 2026-07-13).
  2. 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.
  3. Build with the ASAN fuzzing mozconfig (mozconfig.linux.asan.fuzzing) via build_firefox.
  4. 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).
  5. 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.
  6. 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.
Attached file followups.yaml —
Attached file testcase.html —
Attached patch simulation-patch.diff — — Splinter Review
Attached file prefs.json —
Attached file crash_stack.txt —
Attached file prefs.js —
Group: core-security → dom-core-security
Attached file (secure) —
Assignee: nobody → nika
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Component: IPC → DOM: Service Workers

: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?

Flags: needinfo?(nika)
Blocks: 2024537
Duplicate of this bug: 2054724
Duplicate of this bug: 2059511
Group: dom-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 3 months ago
Resolution: --- → FIXED
Target Milestone: --- → 155 Branch

The patch landed in nightly and beta is affected.
:nika, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(nika)
Severity: -- → S3
Priority: -- → P3
Flags: needinfo?(nika)

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
Attachment #9620645 - Flags: approval-mozilla-beta?

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
Attachment #9620646 - Flags: approval-mozilla-esr140?
Attached file (secure) (obsolete) —

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
Attachment #9620647 - Flags: approval-mozilla-esr153?

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
Attachment #9620649 - Flags: approval-mozilla-release?
Attached file (secure) (obsolete) —

Comment on attachment 9620649 [details]
(secure)

This is affecting ESR so not suitable for inclusion in a dot release on the release channel.

Attachment #9620649 - Flags: approval-mozilla-release? → approval-mozilla-release-
Attachment #9620645 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9620649 - Attachment is obsolete: true
Attachment #9620647 - Flags: approval-mozilla-esr153? → approval-mozilla-esr153+

:nika this was also backed out. Are they dependent on each other? (re: bug 2054674)

Flags: needinfo?(nika)
Attachment #9620647 - Flags: approval-mozilla-esr153+ → approval-mozilla-esr153?

(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.

I've updated https://phabricator.services.mozilla.com/D315748 to add the {} arguments., hopefully that works?

Flags: needinfo?(nika) → needinfo?(dsmith)
Attachment #9620647 - Flags: approval-mozilla-esr153? → approval-mozilla-esr153+
Flags: needinfo?(dsmith)

still caused bustage

Flags: needinfo?(nika)

(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.

Flags: needinfo?(nika) → needinfo?(dsmith)
Flags: needinfo?(dsmith)
Attachment #9620646 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Attachment #9620646 - Attachment is obsolete: true

We can't uplift this to 140 as we don't have bug 2039990 there.

Depends on: 2039990
QA Whiteboard: [sec] [uplift] [qa-triage-done-c155/b154]
Whiteboard: [adv-main154+r][adv-esr53.1+r]
Whiteboard: [adv-main154+r][adv-esr53.1+r] → [adv-main154+r][adv-esr153.1+r]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: