5021f999fe Don't log interaction from temporary gesture on storage access rejection
Triage note: A rejected no-gesture storage-access request logged interaction that could silently grant cross-site cookie access, a tracking-prevention bypass.
Contents
The bug at a glance
OBSERVED: DocumentStorageAccess::enableTemporaryTimeUserGesture now constructs its temporary UserGestureIndicator with UserGestureType::ActivationTriggering and, critically, UserGestureIndicator::ProcessInteractionStyle::Never instead of the default Immediate. INFERRED: the no-gesture requestStorageAccess() rejection path installs a temporary gesture so callers keep gesture-gated abilities, but with Immediate it logged user interaction as a side effect; in a popup with an opener that fabricated interaction triggered requestStorageAccessUnderOpener(), silently granting cross-site cookie access with no prompt. Medium: a tracking-prevention / privacy bypass yielding cross-site storage access without consent.
A rejection path fabricated user interaction. To preserve gesture-gated capabilities on the no-gesture rejection branch, WebKit installs a short-lived UserGestureIndicator; but the default ProcessInteractionStyle::Immediate has a side effect of logging the domain as having had user interaction. That logged interaction is exactly the signal ITP uses to auto-grant storage access under an opener. So merely calling requestStorageAccess() without a gesture, and getting rejected, silently whitelisted the tracker.
Root cause
DocumentStorageAccess implements the Storage Access API. When requestStorageAccess() is called without a user gesture, WebKit rejects the promise but deliberately preserves a temporary user gesture so that the caller can still perform other gesture-gated actions (such as window.open) that the platform allows on this path. This is done via enableTemporaryTimeUserGesture(), which constructs an m_temporaryUserGesture UserGestureIndicator.
The pre-patch construction was makeUnique<UserGestureIndicator>(IsProcessingUserGesture::Yes, protect(m_document).ptr()). Relying on the default arguments, this created an ActivationTriggering-style gesture with ProcessInteractionStyle::Immediate. Immediate means the act of installing the indicator immediately logs user interaction for the document’s domain – it records, in the tracking-prevention store, that the user interacted with that origin.
That logged interaction is precisely the input WebKit’s Intelligent Tracking Prevention uses when deciding whether to auto-grant storage access. In a popup that has an opener, the presence of recorded user interaction on the third-party origin causes requestStorageAccessUnderOpener() to fire, which silently grants the popup’s cross-site iframe storage (cookie) access under the opener relationship – with no user prompt. So a no-gesture requestStorageAccess() call, even though the promise itself was rejected, laundered a fake interaction that flipped the origin from blocked to granted, defeating cookie blocking.
The fix passes explicit arguments: UserGestureType::ActivationTriggering and UserGestureIndicator::ProcessInteractionStyle::Never. Never suppresses the interaction-logging side effect while still installing a gesture, so gesture-gated actions remain possible but no fabricated interaction is recorded, and requestStorageAccessUnderOpener() is not spuriously triggered. OBSERVED: the added layout test opens a popup whose cross-site iframe calls requestStorageAccess() without a gesture, confirms the promise is rejected, then verifies the first-party cookie is NOT delivered afterward (storage access was not silently granted).
Key code
Suppress interaction logging on the temporary no-gesture indicator
void DocumentStorageAccess::enableTemporaryTimeUserGesture()
{
- m_temporaryUserGesture = makeUnique<UserGestureIndicator>(IsProcessingUserGesture::Yes, protect(m_document).ptr());
+ m_temporaryUserGesture = makeUnique<UserGestureIndicator>(IsProcessingUserGesture::Yes, protect(m_document).ptr(), UserGestureType::ActivationTriggering, UserGestureIndicator::ProcessInteractionStyle::Never);
}
Patch walkthrough
Source/WebCore/dom/DocumentStorageAccess.cpp— enableTemporaryTimeUserGesture() changed its UserGestureIndicator construction from the default (IsProcessingUserGesture::Yes, document) – which implies ProcessInteractionStyle::Immediate – to explicitly pass UserGestureType::ActivationTriggering and UserGestureIndicator::ProcessInteractionStyle::Never. Never prevents the temporary gesture from logging user interaction, so the no-gesture rejection path no longer fabricates the interaction that ITP would treat as consent to grant storage access under an opener.LayoutTests/http/tests/storageAccess/no-gesture-rejection-should-not-grant-storage-access-under-opener.https.html— Main test: marks a third-party origin prevalent with prior interaction, enables cookie blocking, opens a popup to that origin which embeds a cross-site iframe. The iframe calls requestStorageAccess() with no gesture; the test asserts the request is rejected and then that the first-party cookie is not received, i.e. access was not silently granted.LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-in-iframe.html / request-storage-access-without-gesture-and-report-back.html— Support frames: the iframe invokes document.requestStorageAccess() at load (no gesture) and posts ‘resolved’/‘rejected’ back up; the popup relays the result to the opener for assertion.
Background
Storage Access API — document.requestStorageAccess() lets an embedded cross-site frame request first-party cookie access. WebKit gates it on user activation and tracking-prevention state; the no-gesture path rejects but preserves a temporary gesture for other capabilities.
ProcessInteractionStyle::Immediate vs Never — A UserGestureIndicator option. Immediate logs user interaction with the document’s domain as a side effect of installing the gesture; Never installs the gesture without recording any interaction.
requestStorageAccessUnderOpener() — An ITP mechanism that auto-grants a popup’s cross-site frame storage access based on recorded user interaction with the origin under the opener relationship, without a prompt.
Intelligent Tracking Prevention (ITP) — WebKit’s tracking-prevention subsystem. Recorded user interaction with a prevalent domain is a key input for relaxing cookie blocking; a fabricated interaction therefore directly weakens the policy.
Vulnerability window
- Design — The no-gesture requestStorageAccess() rejection path installs a temporary UserGestureIndicator to keep gesture-gated actions (window.open) available.
- Side effect — Default ProcessInteractionStyle::Immediate caused that temporary gesture to log user interaction for the origin.
- Silent grant — In a popup with an opener, the fabricated interaction triggered requestStorageAccessUnderOpener(), granting cross-site cookie access with no prompt.
- Discovery — bug 309504 / rdar://171546420 traced the silent grant to the interaction logged by the temporary gesture.
- Fix — Construct the temporary gesture with ProcessInteractionStyle::Never so no interaction is logged; test confirms cookies are not delivered post-rejection.
Proof of concept
VERBATIM from request-storage-access-without-gesture-in-iframe.html. A cross-site iframe inside an opened popup calls document.requestStorageAccess() at load with no user gesture. The promise is rejected, but pre-patch the temporary Immediate-style gesture logged interaction, so requestStorageAccessUnderOpener() then silently granted first-party cookie access. The harness verifies the first-party cookie is NOT subsequently received.
function run() {
document.requestStorageAccess().then(
function() {
window.parent.postMessage({
type: "storage-access-result",
result: "resolved",
error: null
}, "*");
},
function(e) {
window.parent.postMessage({
type: "storage-access-result",
result: "rejected",
error: e.toString()
}, "*");
}
);
}
// body onload="run()" -- no user gesture
Exploitation
- Setup — Tracker origin is already prevalent/has prior interaction (as ITP records for common third parties); attacker opens a popup to that origin from the opener.
- Fabricate interaction — The popup’s cross-site iframe calls requestStorageAccess() without a gesture; pre-patch the rejection path’s Immediate gesture logs user interaction for the origin.
- Silent cross-site cookies — requestStorageAccessUnderOpener() auto-grants the frame first-party cookie access with no prompt, enabling cross-site tracking that tracking-prevention was meant to block.
Detection & hunting
For defenders and SOC / detection engineers:
- Interaction logged on no-gesture rejection —
- Unprompted storage grant under opener —
Audit directions
- Temporary gesture constructions —
- Interaction-logging inputs to ITP —
- requestStorageAccessUnderOpener triggers —