Cross-origin container Permissions-Policy overwrite via RecvSetContainerFeaturePolicy
Categories
(Core :: DOM: Security, defect)
Tracking
()
People
(Reporter: iifizzyx, Unassigned)
Details
(Keywords: ai-involved, reporter-external, Whiteboard: [client-bounty-form])
Attachments
(2 files)
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_DEBUGnot 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-1972defines parent IPCSetContainerFeaturePolicy(MaybeDiscardedBrowsingContext, MaybeFeaturePolicyInfo).dom/html/HTMLIFrameElement.cpp:250-270sendsSetContainerFeaturePolicyfor an embedded iframe'sBrowsingContext.dom/base/nsObjectLoadingContent.cpp:1870-1896sends the same IPC for object/embed container policy.dom/ipc/ContentParent.cpp:7907-7918receives the IPC and writes the supplied policy to the supplied canonicalBrowsingContext.docshell/base/CanonicalBrowsingContext.cpp:3196-3199stores theMaybe<FeaturePolicyInfo>intomContainerFeaturePolicyInfo.netwerk/base/LNAPermissionRequest.cpp:135-180readsCanonicalBrowsingContext::GetContainerFeaturePolicy()and cancels Local Network Access when the container policy deniesloopback-networkorlocal-network.
Also reviewed the existing authorization helper:
dom/ipc/ContentParent.cpp:6995-7004:ContentParent::CheckBrowsingContextEmbedder()verifies that the targetBrowsingContextis embedded in the sending content process.dom/ipc/ContentParent.cpp:7006-7019:RecvDiscardBrowsingContextusesCheckBrowsingContextEmbedder()before mutating a targetBrowsingContext.
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:
- Opens a top-level
https://example.commochitest page. - The top-level page embeds a cross-origin victim iframe from
example.orgwithoutallow="loopback-network". - The top-level page performs a loopback fetch and the test clicks the normal
loopback-networkprompt allow button. This creates the temporary top-level LNA permission. - The victim iframe performs the same loopback fetch.
- The request is denied with
NS_ERROR_LOCAL_NETWORK_ACCESS_DENIEDbefore 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. - A cross-origin sibling attacker iframe from
example.netsends the forgedSetContainerFeaturePolicyIPC naming the victimBrowsingContextand supplyingMaybeFeaturePolicyInfo = Nothing(). - The parent process accepts the message and clears the victim
CanonicalBrowsingContextcontainer policy. - The victim iframe repeats the loopback fetch.
- The request succeeds with LNA prompt action
auto_allow, and no newloopback-networkdoorhanger 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 approvalvictim is still blocked by container feature policy despite cached permissionbaseline feature-policy denial does not show an LNA promptattacker sibling's IPC cleared the victim CanonicalBrowsingContext policyvictim reuses the cached LNA permission after forged policy clearforged 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.
| Reporter | ||
Updated•5 months ago
|
| Reporter | ||
Comment 1•5 months ago
|
||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•2 months ago
|
Comment 3•1 month ago
|
||
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.
Comment 4•1 month ago
|
||
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.
Description
•