Bounce Tracking Protection: an iframe that navigates the top level becomes the extended navigation's initialHost and exempts itself from classification
Categories
(Core :: Privacy: Anti-Tracking, defect, P2)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox155 | --- | fixed |
People
(Reporter: emz, Assigned: emz)
References
(Blocks 1 open bug)
Details
Attachments
(2 files)
A cross-site iframe can escape bounce tracker classification by nominating itself as the initialHost of the extended navigation it starts.
Steps to reproduce
A.exampleembeds a cross-site iframe fromB.example.- The frame navigates the top level to
B.example(<a target="_top">,<form target="_top">, orwindow.top.location = ...). B.exampleimmediately server-redirects the top level back toA.example.
Actual result: B.example is never classified. It got a top-level, first-party context - so Set-Cookie on the 302 lands in its unpartitioned jar, readable on every subsequent bounce from any embedder - and the user is back on A.example none the wiser.
Expected result: B.example is classified as a bounce tracker candidate.
Why it happens
BounceTrackingState::OnStartNavigation derives the record's initialHost from nsILoadInfo::triggeringPrincipal (BounceTrackingState.cpp:684, written at :702 and :725). For a navigation initiated by a nested navigable, that is the frame's principal. BounceTrackingProtection::RecordStatefulBounces then skips any bounce-set host equal to initialHost (BounceTrackingProtection.cpp:337), so the frame's site is skipped, and the embedder is skipped as finalHost. Nothing is left to classify. MOZ_LOG:
OnStartNavigation: new BounceTrackingRecord(): {mInitialHost:example.net, ...}
RecordStatefulBounces: Skip host == initialHost: example.net
RecordStatefulBounces: Skip host == finalHost: example.com
No user gesture is required. The !mBounceTrackingRecord branch at :700-702 sets initialHost unconditionally - the hasUserActivation test at :703 only gates AddUserActivationHost - and the record is nulled 10s after every document load (privacy.bounceTrackingProtection.clientBounceDetectionTimerPeriodMS). A hidden iframe scripting top.location is enough.
initialHost is the only frame-derived field in the record. finalHost comes from the committed top-level document, and user activation is attributed to the top level (WindowGlobalParent::RecvRecordUserActivationForBTP uses TopWindowContext()->DocumentPrincipal()). So the same click that is denied B the activation exemption grants it a functionally identical initialHost exemption.
Spec status
Gecko is faithful here: process navigation start step 3 reads "Let |origin| be |sourceDocument|'s origin", and HTML supplies the iframe's document as sourceDocument for both trigger shapes. But record a user activation in the same document deliberately climbs to "|navigable|'s top-level traversable's active document". The sourceDocument wording exists to resolve privacycg/nav-tracking-mitigations#50, whose resolution was "set the initial host ... to the site which the popup was opened from" - i.e. the site the user was on and deliberately left. sourceDocument is a proxy for that which breaks for a nested initiator, so #50's bypass reappears through a different door.
Chromium is not affected. content/browser/btm/btm_bounce_detector.cc:705-711 takes the chain start from delegate_->GetLastCommittedURL(), consulting navigation_handle->GetInitiator() only when the tab has nothing committed, and :1044-1050 ignores subframe activations outright. The current spec text describes neither engine's safe behaviour.
Proposed fix
Derive initialHost from the top-level document the extended navigation started from: this tab's current (pre-navigation) top-level document, falling back to the initiator's top-level document only when the tab has nothing committed (window.open / target=_blank, preserving bug 1843308 / privacycg#50). Gate on the triggering principal being track-worthy so chrome-initiated navigations keep their empty initialHost and gain no new exemption.
Follow-ups, not in scope here
mUserActivationHostsstill receives the frame's site, so a busting frame can still earn itself a dFPI storage-access grant viaDynamicFpiNavigationHeuristic.cpp:151.window.openfrom a cross-site iframe is the same gap through the opener chain.- bug 1926384 -
finalHostself-nomination viawindow.close().
| Assignee | ||
Updated•4 days ago
|
| Assignee | ||
Comment 1•2 days ago
|
||
The bounce tracking record's initial host came from the navigation's triggering
principal, which for a navigation started by a nested navigable is the frame's own
principal. RecordStatefulBounces exempts the initial host, so a cross-site iframe
could navigate the top level through its own origin and back to the embedder without
ever being classified, gaining a first party context to write unpartitioned cookies
from. The spec derives it from the navigation's source document, which is where the
gap comes from; a spec issue will be filed separately.
Use the site the context is leaving instead: the most recent non-initial document
committed in it, via BrowsingContext::GetHasLoadedNonInitialDocument and the current
window context, both updated at commit. A context with no committed document of its
own was opened by this navigation, so its opener is used.
The derivation no longer needs the navigation to have an initiator, so one started by
the browser also exempts the site it is leaving. That fixes bug 2060555.
This matches how user activation is already attributed:
WindowGlobalParent::RecvRecordUserActivationForBTP keys on the top window context, so
a click in a frame is credited to the embedding site. The record's user activation set
deliberately still keeps the triggering principal's site, because it feeds the dynamic
FPI storage access heuristic, which wants the site that was interacted with.
| Assignee | ||
Comment 2•2 days ago
|
||
https://hg.mozilla.org/mozilla-central/rev/1d8bf3c7775e
https://hg.mozilla.org/mozilla-central/rev/7bdd7fc3d925
Description
•