Closed Bug 2043795 Opened 5 months ago Closed 1 month ago

Cross-origin container Permissions-Policy overwrite via RecvSetContainerFeaturePolicy

Categories

(Core :: DOM: Security, defect)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2045868

People

(Reporter: iifizzyx, Unassigned)

Details

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

Attachments

(2 files)

Attached file minimal_poc.patch —

Steps to Reproduce

Source tested:

  • Source archive directory: mozilla-central-08141cafdb33
  • Mercurial node: 08141cafdb33a36773cac97e455293cf272cdc06
  • Archive SHA-256: de12292462e53f03ec7e429673fc952fa660c0d71f6f51994ec713c232ffd699
  • Firefox version: Mozilla Firefox 153.0a1
  • BuildID: 20260530074055
  • Build config observed in config.status: NIGHTLY_BUILD=1, MOZ_OPTIMIZE=1, MOZ_DEBUG not set

Built from the clean tree with the default local build configuration:

./mach build

Code Reviewed

Reviewed the container Permissions-Policy IPC path:

  • dom/ipc/PContent.ipdl:1970-1972 defines parent IPC SetContainerFeaturePolicy(MaybeDiscardedBrowsingContext, MaybeFeaturePolicyInfo).
  • dom/html/HTMLIFrameElement.cpp:250-270 sends SetContainerFeaturePolicy for an embedded iframe's BrowsingContext.
  • dom/base/nsObjectLoadingContent.cpp:1870-1896 sends the same IPC for object/embed container policy.
  • dom/ipc/ContentParent.cpp:7907-7918 receives the IPC and writes the supplied policy to the supplied canonical BrowsingContext.
  • docshell/base/CanonicalBrowsingContext.cpp:3196-3199 stores the Maybe<FeaturePolicyInfo> into mContainerFeaturePolicyInfo.
  • netwerk/base/LNAPermissionRequest.cpp:135-180 reads CanonicalBrowsingContext::GetContainerFeaturePolicy() and cancels Local Network Access when the container policy denies loopback-network or local-network.

Also reviewed the existing authorization helper:

  • dom/ipc/ContentParent.cpp:6995-7004: ContentParent::CheckBrowsingContextEmbedder() verifies that the target BrowsingContext is embedded in the sending content process.
  • dom/ipc/ContentParent.cpp:7006-7019: RecvDiscardBrowsingContext uses CheckBrowsingContextEmbedder() before mutating a target BrowsingContext.

RecvSetContainerFeaturePolicy does not do the same check.

PoC

Wrote a browser mochitest PoC in minimal_poc.patch. The patch adds PoC-only nsIDOMWindowUtils helpers so chrome test code can simulate the compromised content-process primitive.

The attacker iframe process calls:

pocClearContainerFeaturePolicyForTesting(targetBrowsingContextId)

That helper sends:

ContentChild::SendSetContainerFeaturePolicy(target, Nothing());

The patch also adds:

netwerk/test/browser/browser_test_lna_container_policy_ipc_poc.js

and registers it in:

netwerk/test/browser/browser.toml

The browser test does the following:

  1. Opens a top-level https://example.com mochitest page.
  2. The top-level page embeds a cross-origin victim iframe from example.org without allow="loopback-network".
  3. The top-level page performs a loopback fetch and the test clicks the normal loopback-network prompt allow button. This creates the temporary top-level LNA permission.
  4. The victim iframe performs the same loopback fetch.
  5. The request is denied with NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED before any prompt. This verifies that the victim's container feature policy still blocks the iframe even though the top-level site has a cached LNA allow.
  6. A cross-origin sibling attacker iframe from example.net sends the forged SetContainerFeaturePolicy IPC naming the victim BrowsingContext and supplying MaybeFeaturePolicyInfo = Nothing().
  7. The parent process accepts the message and clears the victim CanonicalBrowsingContext container policy.
  8. The victim iframe repeats the loopback fetch.
  9. The request succeeds with LNA prompt action auto_allow, and no new loopback-network doorhanger is shown.

Runtime Note

The final passing PoC does not call the shared setupLnaPrefs() helper. It does not force-enable network.lna.blocking, does not disable network.lna.local-network-to-localhost.skip-checks, does not enable WebSocket LNA, and does not shorten the default LNA temporary permission timeout.

The only LNA-related pref set by the PoC is:

network.lna.address_space.public.override = 127.0.0.1:4443

This is needed because mochitest serves https://example.com from a local 127.0.0.1 test server. The override makes the test origin model a real public-site-to-loopback LNA transition. In real browsing, an actual public site does not need this test-only address-space override.

Test Result

Ran:

./mach test netwerk/test/browser/browser_test_lna_container_policy_ipc_poc.js

Result:

TEST_END: Test PASS. Subtests passed 8/8. Unexpected 0
Browser Chrome Test Summary
    Passed: 8
    Failed: 0
    Todo: 0
Overall Summary
Ran 9 checks (8 subtests, 1 tests)
Expected results: 9
Unexpected results: 0
OK

Relevant passing assertions:

  • top-level request succeeds after user approval
  • victim is still blocked by container feature policy despite cached permission
  • baseline feature-policy denial does not show an LNA prompt
  • attacker sibling's IPC cleared the victim CanonicalBrowsingContext policy
  • victim reuses the cached LNA permission after forged policy clear
  • forged policy clear allows victim access without a new LNA prompt

The browser console recorded the final victim request as:

Local Network Access detected:
top-level site "https://example.com/..."
initiator "https://example.org/..."
target "http://localhost:21555/..."
prompt action: auto_allow

Attacker Primitive

From a compromised content process:

RefPtr<BrowsingContext> target = BrowsingContext::Get(victimBrowsingContextId);
ContentChild::GetSingleton()->SendSetContainerFeaturePolicy(target, Nothing());

There is no JS/WebIDL binding for this primitive. The attacker model is a compromised content process that can send its own PContent messages. The parent process is expected to enforce the BrowsingContext authority boundary.

Actual Results

RecvSetContainerFeaturePolicy accepts a content-supplied BrowsingContext and a content-supplied MaybeFeaturePolicyInfo, then writes it to the canonical BrowsingContext without verifying that the sender embeds the target context.

dom/ipc/ContentParent.cpp:7907-7918:

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 missing check is the same one used by nearby BrowsingContext mutation IPC.

dom/ipc/ContentParent.cpp:6995-7004:

bool ContentParent::CheckBrowsingContextEmbedder(CanonicalBrowsingContext* aBC,
                                                 const char* aOperation) const {
  if (!aBC->IsEmbeddedInProcess(ChildID())) {
    MOZ_LOG(BrowsingContext::GetLog(), LogLevel::Warning,
            ("ParentIPC: Trying to %s out of process context 0x%08" PRIx64,
             aOperation, aBC->Id()));
    return false;
  }
  return true;
}

dom/ipc/ContentParent.cpp:7012-7014 shows the intended pattern:

if (!CheckBrowsingContextEmbedder(context, "discard")) {
  return IPC_FAIL(this, "Illegal Discard attempt");
}

The sink is docshell/base/CanonicalBrowsingContext.cpp:3196-3199:

void CanonicalBrowsingContext::SetContainerFeaturePolicy(
    Maybe<FeaturePolicyInfo>&& aContainerFeaturePolicyInfo) {
  mContainerFeaturePolicyInfo = std::move(aContainerFeaturePolicyInfo);
}

Because the IPC argument is MaybeFeaturePolicyInfo, the attacker can send Nothing(). This clears the parent-side container policy entirely.

LNAPermissionRequest then reads the now-cleared policy.

netwerk/base/LNAPermissionRequest.cpp:158-179:

Maybe<dom::FeaturePolicyInfo> fpInfo = bc->GetContainerFeaturePolicy();
...
if (fpInfo.isSome()) {
  nsAutoString featureName;
  if (mType.Equals(LOOPBACK_NETWORK_PERMISSION_KEY)) {
    featureName = u"loopback-network"_ns;
  } else {
    featureName = u"local-network"_ns;
  }

  if (fpInfo->mInheritedDeniedFeatureNames.Contains(featureName)) {
    NS_WARNING("Feature policy denying the request");
    return Cancel();
  }
}

When fpInfo is Nothing(), this block is skipped. The code comment at LNAPermissionRequest.cpp:159-166 says missing feature policy is safe because it would only mean the request is from a top-level document. That assumption is not true if a non-embedder content process can clear another frame's CanonicalBrowsingContext policy over IPC.

Observable Post-Conditions

Before forged IPC:

victim iframe without allow="loopback-network"
top-level site already has cached loopback-network allow
victim fetch -> NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED
no LNA prompt shown
console prompt action: auto_deny

After forged IPC:

attacker sibling clears victim mContainerFeaturePolicyInfo with Nothing()
parent-side check confirms GetContainerFeaturePolicy().isNothing()
same victim fetch -> NS_OK
no new LNA prompt shown
console prompt action: auto_allow

Expected Results

The parent process should only accept SetContainerFeaturePolicy from the content process that embeds the target BrowsingContext. A sibling process should not be able to name an arbitrary cross-origin BrowsingContext and replace or clear its container Permissions-Policy state.

Flags: sec-bounty?
Attachment #9591813 - Attachment mime type: text/x-patch → text/plain
Attached file Root_cause.md —
Group: firefox-core-security → dom-core-security
Status: UNCONFIRMED → RESOLVED
Closed: 5 months ago
Component: Security → DOM: Security
Duplicate of bug: 2025192
Product: Firefox → Core
Resolution: --- → DUPLICATE
Flags: sec-bounty? → sec-bounty-
Group: dom-core-security

We do check that the BC has to be related to the sending process, but as this bug points out, that doesn't help if you are in the same BC tree.

Status: RESOLVED → REOPENED
No longer duplicate of bug: 2025192
Ever confirmed: true
Keywords: sec-low
Resolution: DUPLICATE → ---

It looks like we do have a bug on file that has been duped to a bunch of times. This one was filed slightly earlier, but as it is now, the bug won't qualify for a bounty so the order of filing doesn't matter too much.

Status: REOPENED → RESOLVED
Closed: 5 months ago → 1 month ago
Duplicate of bug: 2045868
Keywords: sec-low
Resolution: --- → DUPLICATE
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: