← WebKit Silent-Fix Report — 2026-W25

01c89d15c3  CSP 'self' does not match in opaque-origin http(s) documents

severity medium class Bypass confidence 0.60 WebCore CSP exploitable-grade
Roberto Rodriguez Mon Jun 15 15:40:20 2026 -0700 full: 01c89d15c3f8e75934784f53bf7816bc6c6fb636 bug report ↗ view on GitHub ↗
Primitive: CSP 'self' matching in opaque-origin documents
Triage note: CSP source-expression matching correctness after redirect in opaque-origin documents is policy-enforcement behavior with security impact.
Contents

The bug at a glance

This corrects how CSP resolves the ‘self’ source expression for http(s) documents that have an opaque origin (e.g. a sandboxed iframe without allow-same-origin, or after a cross-origin redirect). A regression (314912@main) had ‘self’ match nothing, over-blocking same-origin resources; more broadly, incorrect ‘self’ resolution against a hostless opaque origin is a CSP-enforcement correctness bug where both over-blocking and unintended matching are possible. Impact is policy-enforcement, not memory safety, so medium is appropriate; the demonstrated effect is same-origin content wrongly refused.

CSP3 says a policy’s self-origin is the response URL’s origin, but WebKit was resolving ‘self’ from the document’s runtime SecurityOrigin. For an http(s) document with an opaque origin (sandboxed iframe, or a document arrived at via redirect), that runtime origin has no host, so ‘self’ matched nothing. The angle is deriving ‘self’ from the response URL (m_protectedURL) instead of the opaque origin for exactly the http(s)-opaque case.

Root cause

The ContentSecurityPolicy constructor initializes the ‘self’ source expression by calling updateSourceSelf. Before the patch it always used the ScriptExecutionContext’s runtime SecurityOrigin: updateSourceSelf(*protect(scriptExecutionContext.securityOrigin())). A regression from 314912@main meant that for an http(s) document whose origin is opaque — the canonical case being an <iframe sandbox> without allow-same-origin, but also documents reached through a cross-origin redirect — that opaque origin carries no host/scheme suitable for ‘self’ matching. As a result CSPSource matching for ‘self’ matched nothing, so same-origin scripts, styles, images, and nested frames were all refused, breaking legitimate content.

Per CSP3 2.2.2, the ‘self’ keyword must resolve to the origin of the policy’s response URL, not to a possibly-opaque runtime origin. The fix adds a branch in the constructor: it computes bool hasOpaqueOriginWithResponseURL = scriptExecutionContext.securityOrigin()->isOpaque() && m_protectedURL.protocolIsInHTTPFamily(). When true, it derives ‘self’ from the response URL via updateSourceSelf(SecurityOrigin::create(m_protectedURL).get()); otherwise it keeps the prior behavior of resolving ‘self’ through the context’s own SecurityOrigin.

The condition is deliberately narrow. It only overrides for opaque origins whose protectedURL is in the HTTP family, so ordinary (non-opaque) documents are unaffected and continue to use their real origin. Local-scheme opaque documents — about:blank, srcdoc, blob:, etc. — are intentionally excluded (their URLs are not http(s)), so they keep inheriting ‘self’ from the parent via Document::initSecurityContext rather than being given a meaningless URL-derived self. The added WPT tests confirm the intended semantics: in a sandboxed iframe, same-origin script/style/image/frame with ‘self’ are allowed, cross-origin script is still blocked, and ‘self’ resolves to the final response origin even after a cross-origin redirect.

Key code

Resolving CSP ‘self’ from the response URL for opaque http(s) origins (ContentSecurityPolicy.cpp).

    ASSERT(scriptExecutionContext.securityOrigin());
    // CSP3 2.2.2: a policy's self-origin is the response URL's origin. Apply when the runtime
    // origin is opaque and the URL is http(s); local schemes inherit via Document::initSecurityContext.
    bool hasOpaqueOriginWithResponseURL = scriptExecutionContext.securityOrigin()->isOpaque() && m_protectedURL.protocolIsInHTTPFamily();
    if (hasOpaqueOriginWithResponseURL)
        updateSourceSelf(SecurityOrigin::create(m_protectedURL).get());
    else
        updateSourceSelf(*protect(scriptExecutionContext.securityOrigin()));

Patch walkthrough

  • Source/WebCore/page/csp/ContentSecurityPolicy.cpp — In the ContentSecurityPolicy constructor, replaces the unconditional updateSourceSelf(context origin) with a check: if the runtime security origin isOpaque() and m_protectedURL is http(s), resolve ‘self’ from SecurityOrigin::create(m_protectedURL) (the response URL’s origin per CSP3 2.2.2); otherwise keep resolving ‘self’ from the context’s own origin. This restores same-origin ‘self’ matching in opaque-origin http(s) documents while leaving non-opaque and local-scheme documents unchanged.
  • LayoutTests/imported/w3c/web-platform-tests/content-security-policy/sandbox/* — Adds a suite of WPT sandbox tests and support files verifying ‘self’ behavior in sandboxed (opaque-origin) iframes: same-origin script/img/style/frame allowed, cross-origin script blocked, header-delivered CSP, and ‘self’ resolving to the final origin after a cross-origin redirect.

Background

CSP ‘self’ source expression — In Content Security Policy, the ‘self’ keyword in directives like script-src/img-src/style-src/frame-src matches resources from the same origin as the protected document. How ‘self’ is resolved determines which same-origin loads are permitted and which cross-origin loads are blocked, so mis-resolution directly changes enforcement.

Opaque origin — A SecurityOrigin can be opaque (isOpaque()), meaning it is a unique, hostless origin that is not same-origin with anything except itself. Sandboxed iframes without allow-same-origin, and some redirect scenarios, run in opaque origins. Resolving a host-based construct like CSP ‘self’ against a hostless opaque origin is ill-defined.

CSP3 2.2.2 (self-origin = response URL origin) — The CSP3 spec defines a policy’s ‘self’ relative to the origin of the response URL that delivered the resource, not the environment’s possibly-opaque runtime origin. WebKit’s prior code diverged by using the runtime origin, which broke down once that origin was opaque.

m_protectedURL — The ContentSecurityPolicy stores the URL of the resource it protects (the response URL). Deriving ‘self’ via SecurityOrigin::create(m_protectedURL) recovers a concrete scheme/host/port for matching even when the document’s runtime origin is opaque, aligning with the spec.

Document::initSecurityContext inheritance — For local-scheme documents (about:blank, srcdoc, blob:), the security context and thus ‘self’ are inherited from the initiating/parent document rather than derived from the (non-http) URL. The fix’s http(s)-only condition preserves this inheritance path for local schemes.

Vulnerability window

  1. Prior behavior — CSP ‘self’ in sandboxed/opaque http(s) documents resolved correctly enough for same-origin loads under the older logic.
  2. Regression introduced — 314912@main changed origin handling such that ‘self’ resolved against the opaque origin (no host) in http(s) opaque documents, causing ‘self’ to match nothing and refusing same-origin scripts, styles, images, and nested iframes.
  3. Reported — Filed as webkit.org/b/316847 (rdar://178638597), ‘CSP self does not match in opaque-origin http(s) documents’, by Roberto Rodriguez, reviewed by Ryan Reno.
  4. Fix — Constructor now derives ‘self’ from m_protectedURL when the origin is opaque and the URL is http(s), per CSP3 2.2.2, leaving non-opaque and local-scheme documents on their existing paths.
  5. Fix landed — Canonical 315247@main, accompanied by a WPT sandbox test suite covering same-origin allow, cross-origin block, header delivery, and post-redirect ‘self’.

Proof of concept

Added WPT support document (csp-self-and-cross-origin.sub.html) loaded inside an <iframe sandbox=“allow-scripts”> (opaque origin). Its CSP is script-src ‘self’ ‘unsafe-inline’. It loads a same-origin script (iframe-self-pass.js) and a cross-origin script, then reports which loaded. On a pre-patch build ‘self’ resolves to the hostless opaque origin and the same-origin script is wrongly blocked (FAIL); after the fix ‘self’ resolves to the response URL origin, so the same-origin script is allowed and the cross-origin script remains blocked. The redirect variant (iframe-self-after-redirect.sub.html) additionally checks that ‘self’ tracks the final response origin.

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'">
</head>
<body>
    <script>
        window.__scriptLoaded = false;
        window.__crossOriginScriptLoaded = false;
    </script>
    <script src="iframe-self-pass.js"></script>
    <script src="http://{{hosts[alt][]}}:{{ports[http][0]}}/content-security-policy/sandbox/support/iframe-cross-pass.js"></script>
    <script>
        var sameOriginMsg = window.__scriptLoaded
            ? "PASS same-origin script allowed in sandboxed iframe"
            : "FAIL same-origin script blocked in sandboxed iframe";
        var crossOriginMsg = window.__crossOriginScriptLoaded
            ? "FAIL cross-origin script not blocked in sandboxed iframe"
            : "PASS cross-origin script blocked in sandboxed iframe";
        window.parent.postMessage(sameOriginMsg, "*");
        window.parent.postMessage(crossOriginMsg, "*");
    </script>
</body>
</html>

Exploitation

  1. Reachability — Any page can host a sandboxed iframe (or trigger a cross-origin redirect) yielding an opaque-origin http(s) document carrying a CSP with ‘self’.
  2. Primary observed effect (over-block) — On the regressed builds ‘self’ matches nothing, so same-origin scripts/styles/images/frames are refused. This is a functionality/availability defect for legitimate same-origin content in sandboxed contexts, not a memory-safety issue.
  3. Enforcement-correctness concern — Because mis-resolving ‘self’ against a hostless origin makes matching ill-defined, the general class also risks a policy author’s ‘self’ intent being applied incorrectly; the added cross-origin-block test exists precisely to confirm the fix does not over-match cross-origin. No bypass primitive is demonstrated by the patch.
  4. No crash / no memory corruption — This is entirely a policy-logic change; there is no crash or corruption angle. Impact is confined to which resource loads CSP permits or refuses.

Detection & hunting

For defenders and SOC / detection engineers:

  • CSP violation reports for same-origin resources in sandboxed frames — A surge of CSP violations blocking same-origin script/style/img/frame loads specifically inside sandboxed (opaque-origin) http(s) iframes indicates the regressed ‘self’-matches-nothing behavior.
  • ‘self’ resolution against opaque origins — Instrument updateSourceSelf / CSPSource construction to log when the source origin is opaque; such cases on http(s) documents are where the mismatch between runtime origin and response URL manifests.
  • Behavior change after redirect — Compare CSP ‘self’ matching before and after cross-origin redirects; inconsistency (self tracking the pre-redirect vs final origin) flags incorrect self-origin derivation.

Audit directions

  • Other origin-derived CSP constructs — Review all CSP source-expression resolution (host-source matching, report-uri/report-to, nonce/hash scoping) for reliance on the runtime SecurityOrigin where CSP3 specifies the response URL’s origin, especially under opaque origins.
  • Opaque-origin handling across security features — Audit other security checks (SOP, fetch, referrer, storage partitioning) that resolve host/scheme from a possibly-opaque origin in sandboxed or post-redirect documents for analogous hostless-origin mismatches.
  • Local-scheme inheritance boundary — Confirm the http(s)-only condition truly excludes about:blank/srcdoc/blob: and that those continue to inherit ‘self’ correctly via Document::initSecurityContext, with no path where a data:/blob: document gets a wrong self from a URL.
  • Regression 314912@main scope — Re-examine everything 314912@main changed about origin handling to ensure no other directive or feature silently regressed ‘self’/origin semantics in the same opaque-origin scenarios.

Before / after

Loading diff…