Open Bug 2045868 Opened 5 months ago Updated 13 days ago

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)

defect

Tracking

()

UNCONFIRMED

People

(Reporter: 0x4lter, Unassigned)

References

(Blocks 1 open bug)

Details

(Keywords: ai-involved, reporter-external, sec-want, Whiteboard: [client-bounty-form])

Attachments

(1 file)

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 ValidatePrincipal on the principals carried inside MaybeFeaturePolicyInfo.

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::InheritPolicy inherited-deny step: a non-declared feature (e.g. camera) is added
    to the child's mInheritedDeniedFeatureNames only if !src->AllowsFeatureInternal(camera, childOrigin).
    A forged permissive container makes src allow it → the inherited-deny is skipped.
  • The child's own AllowsFeatureInternal then hits the eSelf default:
    mDefaultOrigin->Subsumes(childOrigin) where mDefaultOrigin is 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:

  1. With media.navigator.permission.disabled=true, post-forge getUserMedia succeeded 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).
  2. With that pref removed and a real persistent camera ALLOW pre-seeded for https://victim.poc
    (in permissions.sqlite, the on-disk state a user creates by clicking "Remember this decision"),
    post-forge the cross-origin iframe still reported permission state "prompt" and getUserMedia
    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.
  • permCamera goes "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 → getUserMedia prompts → 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

  1. Build an ASAN Firefox at HEAD 7c07e6f277 (mozconfig in poc/seed4/README.md).
  2. bash poc/seed4/scripts/run-clean2.sh — it: creates a fresh profile with a real persistent
    camera/mic ALLOW for https://victim.poc:8088 (pre-seeded in permissions.sqlite), trusts the
    local CA (secure context), serves the three .poc sites over HTTPS, and launches headless Firefox
    on https://attacker.poc:8088/.
  3. The top page embeds evil.poc and victim.poc (the latter with allow="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
    cameraAllowed false→true and permCamera denied→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's allow="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
    from media.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=true provides 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.
  • 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.

Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Component: Security → DOM: Security
Product: Firefox → Core
See Also: → 2025192

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.

Keywords: sec-want

The severity field is not set for this bug.
:freddy, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(fbraun)
Severity: -- → S3
Flags: needinfo?(fbraun)
Priority: -- → P2
Duplicate of this bug: 2057372
Duplicate of this bug: 2054281
Duplicate of this bug: 2060999
See Also: → 2062703

Hi guys, I dont understand at all, is that report dup ?

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.

Duplicate of this bug: 2068660
Flags: sec-bounty? → sec-bounty-
Duplicate of this bug: 2073164
Duplicate of this bug: 2073385
Duplicate of this bug: 2073771
Duplicate of this bug: 2076077

The severity field for this bug is set to S3. However, the following bug duplicate has higher severity:

:freddy, could you consider increasing the severity of this bug to S2?

For more information, please visit BugBot documentation.

Flags: needinfo?(fbraun)
Flags: needinfo?(fbraun)
Duplicate of this bug: 2078079
Duplicate of this bug: 2075654
Summary: Missing embedder check in ContentParent::RecvSetContainerFeaturePolicy lets a compromised content process forge another origin's Feature/Permissions-Policy cross-process → Missing embedder check in ContentParent::RecvSetContainerPermissionsPolicy lets a compromised content process forge another origin's Feature/Permissions-Policy cross-process
Duplicate of this bug: 2043795

Bug 2043795 seems to point out the same basic issue and was filed earlier, if that becomes relevant.

Group: dom-core-security
Duplicate of this bug: 2083330
Duplicate of this bug: 2079927
Duplicate of this bug: 2087897
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: