Closed Bug 2054530 Opened 4 months ago Closed 4 months ago

A compromised content process can overwrite any cross-process document's Permissions-Policy

Categories

(Core :: DOM: Security, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2025192

People

(Reporter: vanirxxx, Unassigned)

Details

(Keywords: reporter-external, Whiteboard: [client-bounty-form])

Attachments

(1 file)

15.73 KB, application/x-zip-compressed
Details
Attached file poc.zip —

Summary

ContentParent::RecvSetContainerFeaturePolicy applies a container (iframe) Permissions-Policy to any BrowsingContext a content process names, with no check that the sender hosts that context's embedder. A compromised content process can therefore grant any cross-process frame a capability (camera / microphone / geolocation / display-capture / fullscreen / …) its real embedder never delegated. Demonstrated end-to-end at runtime: a cross-origin subframe whose Permissions-Policy denies camera overwrites its own container policy and then getUserMedia succeeds.

Affects

  • Component: Core :: DOM: Security (Permissions Policy)
  • Version: Nightly 154.0a1, BuildID 20260703100435, changeset 6a252a86c5bb
  • OS: All (tested Linux)

Root cause

Defect — unauthenticated container-policy write (ContentParent::RecvSetContainerFeaturePolicy, dom/ipc/ContentParent.cpp:7869):

mozilla::ipc::IPCResult ContentParent::RecvSetContainerFeaturePolicy(
    const MaybeDiscardedBrowsingContext& aContainerContext,
    MaybeFeaturePolicyInfo&& aContainerFeaturePolicyInfo) {
  if (aContainerContext.IsNullOrDiscarded()) {
    return IPC_OK();
  }
  auto* context = aContainerContext.get_canonical();
  context->SetContainerFeaturePolicy(std::move(aContainerFeaturePolicyInfo));  // no sender check
  return IPC_OK();
}

The only guard on this async PContent message is IsNullOrDiscarded(). It never checks that the sending ContentParent hosts the embedder of aContainerContext; the attacker supplies both the target BC id (a small integer replicated into every process, not a secret) and the policy. The write is a plain member store — CanonicalBrowsingContext::SetContainerFeaturePolicy (docshell/base/CanonicalBrowsingContext.cpp:3241): mContainerFeaturePolicyInfo = std::move(...) — so this bespoke IPC bypasses the synced-BrowsingContext CheckOnlyOwningProcessCanSet authorization that guards every other cross-process BC field. The only legitimate caller, HTMLIFrameElement, sends only its own child iframe's policy; the parent never enforces that invariant.

Reaching the capability: on the victim frame's next load, DocumentLoadListener copies the stored policy into the LoadInfo (netwerk/ipc/DocumentLoadListener.cpp:2119), and Document::InitFeaturePolicy → FeaturePolicy::InheritPolicy (dom/base/Document.cpp:4070, dom/security/featurepolicy/FeaturePolicy.cpp:73) honors the attacker's mAttributeEnabledFeatureNames with an empty mInheritedDeniedFeatureNames — so an undelegated feature becomes allowed for the subframe's own origin.

Steps to Reproduce

Threat model: a compromised content process (renderer-compromise); a real renderer sends the SetContainerFeaturePolicy PContent message directly.

  1. Top page V (http://v.test) embeds cross-origin subframe A (<iframe src="http://a.test/sub.html">, no allow="camera"). Under Fission, A is in its own process.
  2. A (compromised) sends SetContainerFeaturePolicy(A's own BC, {mAttributeEnabledFeatureNames:[camera,microphone], mInheritedDeniedFeatureNames:[], origins:A}).
  3. A reloads, then calls navigator.mediaDevices.getUserMedia({video:true}).

Actual Results

ASAN build, 2-site Fission harness, media.navigator.streams.fake=true:

POC_SUB BASELINE_GUM_FAIL NotAllowedError ...              <- Permissions-Policy blocks camera (undelegated)
POC_FPFORGE sent SetContainerFeaturePolicy(camera,microphone) for own cross-process BC id=4294967297
POC_SUB ATTACK_GUM_OK tracks=1 label=Default Video Device  <- after forge, getUserMedia SUCCEEDS

Before the forge, A's getUserMedia is rejected by Permissions-Policy; after A overwrites its own container policy via the unauthenticated IPC, it obtains a live camera track — a capability its real embedder never granted. Standalone impact: an undelegated frame can also silently enter fullscreen (not prompt-gated). Because the parent attributes a subframe's getUserMedia to the top origin (browser/actors/WebRTCParent.sys.mjs:95: data.origin = topWindowContext.documentPrincipal.origin), the same primitive composes into silent cross-origin camera/microphone capture under the top page's existing grant — this composition is code-traced, not exercised by the PoC.

Expected Results

A content process must not set the container Permissions-Policy of a BrowsingContext whose embedder it does not host.

PoC / harness details

poc/: top/sub pages (web/top.html, web/sub.html), driver (go2.sh), and poc.patch. The patch sends the forged SetContainerFeaturePolicy PContent message from the subframe's own process when the page calls window.dump("FORGE_FP"), gated on env MOZ_POC_FPTAMPER (dom/base/nsGlobalWindowInner.cpp) — standing in for a compromised renderer forging the IPC. The parent handler under test is unmodified. Verbatim run: poc/run.log.

Fix

In RecvSetContainerFeaturePolicy, verify the sending ContentParent hosts the embedder of aContainerContext before applying the policy — mirroring the CheckOnlyOwningProcessCanSet authorization used for every other synced BrowsingContext field.

Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Status: UNCONFIRMED → RESOLVED
Closed: 4 months ago
Component: Security → DOM: Security
Duplicate of bug: 2025192
Product: Firefox → Core
Resolution: --- → DUPLICATE
Flags: sec-bounty? → sec-bounty-
Group: dom-core-security
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: