Missing embedder check in ContentParent::RecvSetContainerPermissionsPolicy lets a compromised content process forge another origin's Feature/Permissions-Policy cross-process
Categories
(Core :: DOM: Security, defect, P2)
Tracking
()
People
(Reporter: 0x4lter, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: ai-involved, reporter-external, sec-want, Whiteboard: [client-bounty-form])
Attachments
(1 file)
|
739.62 KB,
application/x-zip-compressed
|
Details |
Summary
ContentParent::RecvSetContainerFeaturePolicy — the parent-side IPC handler that stores the
Feature/Permissions-Policy an embedder imposes on an embedded frame — performs no embedder
authorization check and no principal validation. It accepts the message from any content process
subscribed to the target BrowsingContext's group and writes the supplied policy onto a
BrowsingContext the sender does not embed.
A compromised content process can therefore overwrite a cross-origin sibling iframe's container
feature policy, flipping a capability the embedder explicitly denied (e.g. allow="camera 'none'")
from DENY → ALLOW. This bypasses the embedder's feature-policy restriction on a cross-origin
victim frame.
This is an absence-of-check bug: the sibling handler RecvDiscardBrowsingContext enforces
CheckBrowsingContextEmbedder; this handler omits it. The defect is present and unfixed at
mozilla-central HEAD 7c07e6f277dd6d5122183d6feecb7473277ab5bb.
Affected code
dom/ipc/ContentParent.cpp — the handler, in full:
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));
return IPC_OK();
}
The only check is IsNullOrDiscarded(). It omits:
CheckBrowsingContextEmbedder(context, ...)→IsEmbeddedInProcess(ChildID())— the guard that
verifies the sending process actually embeds the target BrowsingContext.- Any
ValidatePrincipalon the principals carried insideMaybeFeaturePolicyInfo.
The sibling handler does enforce it (same MaybeDiscarded<BrowsingContext> parameter shape,
opposite trust posture):
mozilla::ipc::IPCResult ContentParent::RecvDiscardBrowsingContext(
const MaybeDiscarded<BrowsingContext>& aContext, bool aDoDiscard, ...) {
if (CanonicalBrowsingContext* context = ...) {
if (aDoDiscard && !context->IsDiscarded()) {
if (!CheckBrowsingContextEmbedder(context, "discard")) {
return IPC_FAIL(this, "Illegal Discard attempt"); // <-- enforced here
}
...
CheckBrowsingContextEmbedder (ContentParent.cpp:7004) has exactly one caller in this
150+-handler file — RecvDiscardBrowsingContext. The container-feature-policy handler relies solely
on the group-subscription floor every BrowsingContext-typed IPC parameter already has.
Reachability / threat model
Threat model: a single compromised content process (Mozilla's standard sandbox/Fission assumption),
invoking the IPC method with attacker-chosen parameters.
The only reachability gate on the BrowsingContext parameter is
ParamTraits<MaybeDiscarded<BrowsingContext>>::Read → Group()->IsKnownForMessageReader
(BrowsingContextGroup.cpp:273-274), which on a PContent parent-side message passes iff the sender
is subscribed to the target's BrowsingContext group — not embedder, not same-origin.
Two cross-origin sibling iframes in one tab share the top page's single BrowsingContextGroup, and
both hosting processes subscribe to it. So a compromised non-embedder process can resolve a victim
BrowsingContext it does not embed and overwrite its container feature policy. Reach is bounded to
the same tab-tree / opener-linked group — not arbitrary tabs.
Root cause — the stored value GRANTS, it does not merely restrict
The Fission child consumes the attacker-stored container info and builds its inherited policy
entirely from the attacker's fields, with no intersection against the real embedder chain:
FeaturePolicy::InheritPolicyinherited-deny step: a non-declared feature (e.g.camera) is added
to the child'smInheritedDeniedFeatureNamesonly if!src->AllowsFeatureInternal(camera, childOrigin).
A forged permissive container makessrcallow it → the inherited-deny is skipped.- The child's own
AllowsFeatureInternalthen hits theeSelfdefault:
mDefaultOrigin->Subsumes(childOrigin)wheremDefaultOriginis the child's own NodePrincipal →
child subsumes child = TRUE → feature GRANTED. - A legitimate cross-origin container without delegation resolves the same default against the
parent origin (parent ≠ child → false → DENIED).
So a non-embedder same-group process flips an embedder-imposed DENY → ALLOW. Confirmed grant, not
restriction (runtime evidence below).
Impact
Feature/Permissions-Policy is the mechanism a page uses to deny capabilities to embedded content
(<iframe allow="camera 'none'">), and it is the pre-prompt hard gate for camera/microphone
(MediaManager.cpp:3144-3155: NotAllowedError before any permission/prompt) and Local Network
Access (LNAPermissionRequest.cpp:186-191: hard-Cancel() before the prompt).
A compromised non-embedder content process can flip a cross-origin victim iframe's camera policy
gate from the embedder-imposed DENY → ALLOW. The measured consequence (see "Runtime evidence"):
the victim's camera goes from a feature-policy hard-block (getUserMedia → NotAllowedError, no
prompt) to a promptable state (getUserMedia reaches the user permission prompt). In effect, a
compromised content process bypasses an embedder's explicit allow="camera 'none'" restriction
on a cross-origin sibling frame and on Local Network Access removes the embedder's hard-deny,
exposing the victim to a prompt it was supposed to be shielded from.
Silent (no-prompt) capture is NOT achieved (see "What is and isn't demonstrated" and "Severity"):
even with a genuine persistent camera grant for the victim origin, the cross-origin third-party
iframe still requires a fresh prompt, so the forge restores promptability, not silent access.
Severity
Proposed sec-moderate. A compromised content process bypasses a cross-origin embedder's
feature-policy restriction on a sibling iframe (the missing embedder authorization that every sibling
handler enforces), forcing a capability gate the embedder denied into a promptable state. This is a
cross-origin / Fission policy-boundary violation; it is not memory corruption and no parent code
execution is claimed.
Why not sec-high (disconfirmed under rule-clean testing). An earlier draft proposed sec-high on
the theory that, given a pre-existing persistent grant, forging the feature-policy gate open yields
silent camera capture. That theory was tested two ways:
- With
media.navigator.permission.disabled=true, post-forgegetUserMediasucceeded silently.
But that pref globally disables the camera permission prompt — a disabled security control — so it
is not valid evidence of impact (it manufactures the "silent" outcome rather than the bug doing
so). - With that pref removed and a real persistent camera ALLOW pre-seeded for
https://victim.poc
(inpermissions.sqlite, the on-disk state a user creates by clicking "Remember this decision"),
post-forge the cross-origin iframe still reported permission state"prompt"andgetUserMedia
prompted (did not silently capture).
So the silent outcome existed only because of the disabled-prompt pref; under faithful conditions
Firefox still prompts for camera in a cross-origin iframe. The honest impact is the promptability
bypass → sec-moderate. Final rating is Mozilla's call.
Runtime evidence
Build: obj-asan-browser (ASAN 153.0a1, HEAD 7c07e6f277), headless, Fission, three cross-origin
.poc sites in separate content processes, parent process UNMODIFIED. The compromised content
process is simulated by an env-gated content-side injection of the IPC send (standard Fission-IPC
research model — no content RCE; the parent handler under test is unmodified). No disabled security
prefs. A real persistent camera/microphone ALLOW is pre-seeded for https://victim.poc:8088.
moz_perms (real persistent grant, kept by Firefox across the run):
https://victim.poc:8088 camera ALLOW
https://victim.poc:8088 microphone ALLOW
cross-process forge fired (evil, non-embedder of victim):
[SEED4] evil forging container policy: victim BC id=8589934594 name=seed4victim (denied-list EMPTY)
victim.poc, embedder-restricted with allow="camera 'none'":
baseline : cameraAllowed=false permCamera="denied" getUserMedia=NotAllowedError (feature-policy hard-block, control)
postforge: cameraAllowed=true permCamera="prompt" getUserMedia=PROMPT(timeout) (gate flipped; now promptable)
cameraAllowed(document.featurePolicy.allowsFeature('camera')) flips false → true purely from
a forged container policy written by a non-embedder process = the confirmed cross-process gate
bypass.permCameragoes "denied" → "prompt" (not "granted") despite the real ALLOW grant existing:
Firefox does not honor a first-party persistent grant for camera in a cross-origin third-party
iframe →getUserMediaprompts → headless cannot answer → timeout. This is the honest
sec-moderate ceiling and the disconfirmation of the silent/sec-high theory.
Parent-side dispatch (gdb, separate run) confirms no embedder/principal check on the path:
#0 CanonicalBrowsingContext::SetContainerFeaturePolicy CanonicalBrowsingContext.cpp:3218
#1 ContentParent::RecvSetContainerFeaturePolicy ContentParent.cpp:7924 <- vulnerable handler
#2 PContentParent::OnMessageReceived PContentParent.cpp:15576 <- arrived over the wire
Evidence files: poc/seed4/evidence/{clean2-run.log, clean-run.log, EVIDENCE.md}, poc/seed4/
(pages, certs, scripts run-clean2.sh/run-clean.sh, patch/seed4-content-inject.patch = the
content-side compromise simulation; parent untouched).
Reproduction
- Build an ASAN Firefox at HEAD
7c07e6f277(mozconfig inpoc/seed4/README.md). bash poc/seed4/scripts/run-clean2.sh— it: creates a fresh profile with a real persistent
camera/mic ALLOW forhttps://victim.poc:8088(pre-seeded inpermissions.sqlite), trusts the
local CA (secure context), serves the three.pocsites over HTTPS, and launches headless Firefox
onhttps://attacker.poc:8088/.- The top page embeds
evil.pocandvictim.poc(the latter withallow="camera 'none'") in
separate processes, reports the victim baseline, has the evil (non-embedder) process forge the
victim's container feature policy, reloads the victim, and reports post-forge. Observe
cameraAllowedfalse→true andpermCameradenied→prompt.
What is and isn't demonstrated
- Demonstrated (runtime, unmodified parent, no disabled flags): the missing embedder check; the
cross-process write onto a non-embedded victim BC; the capability gate flip
(allowsFeature('camera')false→true), which bypasses the embedder'sallow="camera 'none'". - Disconfirmed: silent camera capture. With a real persistent grant and no prompt-disable pref,
the cross-origin iframe still prompts (permission state "prompt"). The only "silent" result came
frommedia.navigator.permission.disabled=true(a disabled control) and is therefore not valid
evidence. - Simulated, clearly labelled: the compromised content process (IPC send injected via an
env-gated content-side patch; no content RCE — Mozilla's rule permits assuming content compromise- invoking an IPC method with attacker parameters).
media.navigator.streams.fake=trueprovides a
synthetic device (accepted test stand-in; WSL2 has no camera passthrough) — but note the gate-flip
result does not depend on a device at all.
- invoking an IPC method with attacker parameters).
- Source-confirmed but not runtime-shown: the Local Network Access hard-deny removal
(LNAPermissionRequest.cpp:186-191); the runtime LNA differential did not reproduce (victim
resolved to a loopback origin, never LNA-gated). - Not claimed: parent code execution, memory corruption, or any silent capability grant.
Rule-compliance note
This finding relies on no disabled security flag and no parent-code patch as evidence of impact.
The content-side injection only performs the cross-process IPC send a compromised content process
would issue (parent handler unmodified). The permission grant is a real persistent ALLOW (the state a
user creates with "Remember this decision"), not a prompt-disable. The fake media device is an
accepted test stand-in and is not load-bearing for the demonstrated result.
Suggested fix
Add the embedder authorization the sibling handler already uses, at the top of the handler:
auto* context = aContainerContext.get_canonical();
if (!CheckBrowsingContextEmbedder(context, "SetContainerFeaturePolicy")) {
return IPC_FAIL(this, "Illegal SetContainerFeaturePolicy attempt");
}
and validate the principals carried in MaybeFeaturePolicyInfo against the sender
(ValidatePrincipal), matching RecvCreateWindow / blob-URL registration.
Updated•5 months ago
|
Comment 1•5 months ago
|
||
Bug 2025192 has a bunch of dupes which are reporting similar issues. This report does acknowledge the new-ish IsKnownForMessageReader check. I guess the point is that it doesn't protect you if you are in the same group as the page you are attacking? Anyways it doesn't seem that bad.
Comment 2•4 months ago
|
||
The severity field is not set for this bug.
:freddy, could you have a look please?
For more information, please visit BugBot documentation.
Updated•4 months ago
|
Updated•4 months ago
|
Comment 6•3 months ago
|
||
Hi guys, I dont understand at all, is that report dup ?
Comment 7•3 months ago
|
||
Yes, bug 2060999 has been marked as a duplicate of this bug. If you think that was done in error, please comment on bug 2060999. Thanks.
Updated•2 months ago
|
Comment 13•1 month ago
|
||
The severity field for this bug is set to S3. However, the following bug duplicate has higher severity:
- Bug 2076077: S2
:freddy, could you consider increasing the severity of this bug to S2?
For more information, please visit BugBot documentation.
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Comment 17•1 month ago
|
||
Bug 2043795 seems to point out the same basic issue and was filed earlier, if that becomes relevant.
Updated•1 month ago
|
Description
•