← WebKit Silent-Fix Report — 2026-W23

093f34607a3e8c6bdf6fbe178a68ca96ecc067c6  Initiator-omitted samesite classification can lead to SameSite=Strict cookie cross-site leakage

severity medium class CrossOrigin confidence 0.90 FrameLoader cookies exploitable-grade
Charlie Wolfe Tue Jun 2 12:52:50 2026 -0700 full: 093f34607a3e8c6bdf6fbe178a68ca96ecc067c6 bug report ↗ view on GitHub ↗
Primitive: SameSite=Strict cookies leak cross-site when initiator omitted
Triage note: FrameLoader::load called addSameSiteInfoToRequestIfNeeded without an initiator, so initiator-omitted navigations were classified as same-site; fix passes request.requester() (unless it inherits origin from owner), preventing cross-site sending of SameSite=Strict cookies.
Contents

The bug at a glance

Any web page can trigger the vulnerable path by causing a browser-initiated top-level navigation (the common case for user-typed URLs, bookmarks, and reloads) to a victim origin, causing SameSite=Strict cookies to be transmitted even though the navigation crossed a site boundary. The impact is confidentiality-only leakage of authentication and session cookies that the SameSite=Strict directive is specifically meant to withhold on cross-site navigations, enabling CSRF-style and session-fixation follow-ons; there is no integrity or availability impact and no code execution, which keeps it at Medium/6.5.

SameSite=Strict is supposed to be the one cookie attribute a developer can trust to never ride along on a cross-site navigation. This bug shows that WebKit’s own navigation entry point quietly told the cookie layer “this is same-site” for every load that lacked an initiator document, so the later, correct, initiator-aware classification never got to run. The result is that a Strict cookie set by victim.example is attached to a victim.example request made immediately after the user was on attacker.example, defeating the entire point of the attribute. The fix is a one-line change to pass the real requester document, but the mechanism is a textbook case of a defaulted-to-safe value that was actually defaulted-to-unsafe.

Root cause

The SameSite disposition of an outgoing request is computed from an initiator document. WebCore models three states via NetworkStorageSession: same-site, cross-site, and “unspecified”. addSameSiteInfoToRequestIfNeeded stamps a disposition onto a ResourceRequest; if it is called with no initiator it takes the conservative-for-functionality-but-dangerous-for-privacy default of forcing isSameSite = true on the request. A request whose SameSite state has already been concretely set (true or false) is no longer isSameSiteUnspecified, so downstream code will not revisit it.

In FrameLoader::load (Source/WebCore/loader/FrameLoader.cpp), the pre-patch code called addSameSiteInfoToRequestIfNeeded(loader->request()) with a single argument and thus no initiator. This unconditionally set isSameSite = true on the document loader’s request. The later, initiator-aware recomputation performed in updateRequestAndAddExtraFields is gated on the request still being isSameSiteUnspecified; because FrameLoader::load had already pinned the value to true, that gate failed and the correct cross-site determination never executed.

The reaching path is any navigation that flows through FrameLoader::load without an explicit initiator being threaded into the SameSite call, most importantly browser/embedder-initiated top-level navigations (the WKWebView loadRequest: path exercised by the regression test). Such a load to victim.example, following a document on attacker.example, would be classified as same-site and would therefore include victim.example’s SameSite=Strict cookies, leaking them across the site boundary.

The fix threads the FrameLoadRequest’s requester document into the call: Ref initiator = request.requester(); then addSameSiteInfoToRequestIfNeeded(loader->request(), SecurityPolicy::shouldInheritSecurityOriginFromOwner(initiator->url()) ? nullptr : initiator.ptr()). When a real requester exists, its origin is used to compute the disposition correctly (yielding cross-site, hence dropping Strict cookies). When the requester is an initial/empty document (about:blank or empty URL, per shouldInheritSecurityOriginFromOwner), nullptr is passed to preserve the same-site default that fresh navigations legitimately need.

Key code

FrameLoader::load now passes the requester document as the SameSite initiator (nullptr only for initial documents).

-    addSameSiteInfoToRequestIfNeeded(loader->request());
+    Ref initiator = request.requester();
+    addSameSiteInfoToRequestIfNeeded(loader->request(), SecurityPolicy::shouldInheritSecurityOriginFromOwner(initiator->url()) ? nullptr : initiator.ptr());

Patch walkthrough

  • Source/WebCore/loader/FrameLoader.cpp — In FrameLoader::load, the bare addSameSiteInfoToRequestIfNeeded(loader->request()) call is replaced with one that captures request.requester() as initiator and forwards it as the second argument. A guard on SecurityPolicy::shouldInheritSecurityOriginFromOwner(initiator->url()) passes nullptr for initial documents (about:blank/empty) so their fresh navigations keep the same-site default, and otherwise passes initiator.ptr() so the disposition is computed against the real requester origin. This lets the request remain SameSite-unspecified when appropriate so the later updateRequestAndAddExtraFields recomputation runs.
  • Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKHTTPCookieStore.mm — Adds TEST(WKHTTPCookieStore, SameSiteStrictCookieNotSentOnCrossSiteNavigation). It sets a SameSite=Strict cookie on victim.example, confirms it is sent on a same-site follow-up navigation (EXPECT_TRUE(sameSiteCheckHasCookie)), navigates to attacker.example, then navigates back to a victim.example check endpoint and asserts the Strict cookie is NOT present (EXPECT_FALSE(crossSiteCheckHasCookie)). The cross-site assertion is what fails before the patch.

Background

SameSite=Strict — A cookie attribute instructing the browser to withhold the cookie from any request whose top-level browsing context site differs from the cookie’s site, including on cross-site top-level navigations. It is the primary declarative defense against CSRF and cross-site session leakage.

isSameSiteUnspecified / addSameSiteInfoToRequestIfNeeded — WebCore tracks a request’s SameSite disposition as same-site, cross-site, or unspecified. addSameSiteInfoToRequestIfNeeded sets it from an initiator origin, or forces same-site when no initiator is supplied. Only requests still marked unspecified are later recomputed.

updateRequestAndAddExtraFields — The FrameLoader routine that, among other extra-field work, performs the initiator-aware SameSite recomputation. It only acts when the request is still isSameSiteUnspecified, so an early forced disposition permanently suppresses it.

SecurityPolicy::shouldInheritSecurityOriginFromOwner — Returns true for URLs like about:blank and the empty URL whose documents inherit their security origin from an owning/creator document. The patch uses it to distinguish a genuine requester from an initial document that should retain the same-site default.

Vulnerability window

  1. Cookie planted — The user visits victim.example, which sets id=secret; SameSite=Strict.
  2. Attacker context — The user later navigates to attacker.example (or an attacker-controlled page is the current document).
  3. Cross-site navigation — A browser-initiated top-level navigation to victim.example flows through FrameLoader::load with no initiator threaded into the SameSite call.
  4. MisclassificationaddSameSiteInfoToRequestIfNeeded forces isSameSite = true, clearing the unspecified state and suppressing the initiator-aware recomputation in updateRequestAndAddExtraFields.
  5. Leak — The request to victim.example is treated as same-site and the SameSite=Strict cookie is attached and sent across the site boundary.
  6. Fix — The patch forwards request.requester() (nullptr only for initial documents), so the disposition is computed as cross-site and the Strict cookie is correctly withheld.

Proof of concept

The patch’s own TestWebKitAPI test is a working proof: it plants a Strict cookie, verifies it rides same-site requests, then shows that after visiting attacker.example a subsequent load of victim.example must not carry the cookie. On the vulnerable build the /cross-site-check request arrives with id=secret; on the fixed build it does not. No memory primitives are involved; the observable is the presence of the cookie header on the server side.

// Reconstructed from the regression test.
// 1. Set a SameSite=Strict cookie on victim.example.
[webView loadRequest:[NSURLRequest requestWithURL:
    [NSURL URLWithString:@"http://victim.example/setcookie"]]];
[webView _test_waitForDidFinishNavigation];

// 2. Same-site navigation: cookie is (correctly) present.
[webView loadRequest:[NSURLRequest requestWithURL:
    [NSURL URLWithString:@"http://victim.example/same-site-check"]]];
// server observes id=secret -> EXPECT_TRUE

// 3. Move to attacker origin.
[webView loadRequest:[NSURLRequest requestWithURL:
    [NSURL URLWithString:@"http://attacker.example/attacker"]]];
[webView _test_waitForDidFinishNavigation];

// 4. Cross-site navigation back to victim.example.
[webView loadRequest:[NSURLRequest requestWithURL:
    [NSURL URLWithString:@"http://victim.example/cross-site-check"]]];
// Pre-patch: server observes id=secret (LEAK). Post-patch: absent -> EXPECT_FALSE

Exploitation

  1. Setup — The attacker relies on the victim having an authenticated session cookie on a target site that is marked SameSite=Strict, and on being able to trigger a top-level navigation to that site through a path that reaches FrameLoader::load without an initiator (embedder-driven loads, certain address-bar/bookmark style loads).
  2. Trigger — From an attacker-controlled document, cause a navigation to the target origin. Because the load is misclassified as same-site, the Strict cookie is attached to the outgoing request.
  3. Payoff — A server the attacker can observe (or a target endpoint that reflects state) receives the Strict-scoped cookie, enabling CSRF against Strict-protected endpoints or theft/confirmation of the session identifier that Strict was meant to keep from cross-site contexts.

Detection & hunting

For defenders and SOC / detection engineers:

  • Server-side cookie logs
  • FrameLoader instrumentation
  • Regression test

Audit directions

  • Other callers of addSameSiteInfoToRequestIfNeeded
  • Initiator threading through navigation entry points
  • isSameSiteUnspecified gating

Before / after

Loading diff…