← All issues

[2] HTMLResourcePreloader CSP bypass via empty-nonce preloading

Severity: High | Component: WebCore HTML parser — HTMLResourcePreloader | 88d6ffd

Rated High because the observable effect is a complete Content Security Policy bypass for redirected resources loaded after a meta CSP tag, reachable from web content by controlling HTML structure around the meta tag — the empty-nonce evaluation against an absent policy set is confirmed by the fix pattern with confidence 0.92.

When parser-blocking scripts are declared before a meta CSP tag, the preload scanner fetches subsequent resources before the CSP policy is parsed. For each preloaded script/stylesheet, HTMLResourcePreloader calls allowScriptWithNonce/allowStyleWithNonce with an empty nonce, and because no policies exist yet these functions return true, which HTMLResourcePreloader interprets as a valid nonce match and sets ContentSecurityPolicyImposition::SkipPolicyCheck on the resource's ResourceLoaderOptions. This persists on the SubresourceLoader for the resource's lifetime and results in redirect targets that violate the CSP policy loading without being blocked.

Source/WebCore/html/parser/HTMLResourcePreloader.cpp

bool skipContentSecurityPolicyCheck = false;
- if (m_resourceType == CachedResource::Type::Script || m_resourceType == CachedResource::Type::JSON)
- skipContentSecurityPolicyCheck = protect(document.contentSecurityPolicy())->allowScriptWithNonce(m_nonceAttribute);
- else if (m_resourceType == CachedResource::Type::CSSStyleSheet)
- skipContentSecurityPolicyCheck = protect(document.contentSecurityPolicy())->allowStyleWithNonce(m_nonceAttribute);
+ if (!m_nonceAttribute.isEmpty()) {
+ if (m_resourceType == CachedResource::Type::Script || m_resourceType == CachedResource::Type::JSON)
+ skipContentSecurityPolicyCheck = protect(document.contentSecurityPolicy())->allowScriptWithNonce(m_nonceAttribute);
+ else if (m_resourceType == CachedResource::Type::CSSStyleSheet)
+ skipContentSecurityPolicyCheck = protect(document.contentSecurityPolicy())->allowStyleWithNonce(m_nonceAttribute);
+ }

The fix adds an m_nonceAttribute.isEmpty() guard in PreloadRequest::resourceRequest() so that allowScriptWithNonce / allowStyleWithNonce are only called when the preload request actually carries a nonce. Before the fix, these functions were called unconditionally with an empty nonce string, and when no CSP policies existed yet (because the meta CSP tag had not been parsed), they returned true, causing SkipPolicyCheck to be set on the resource's ResourceLoaderOptions. The test expectation files for ios-site-isolation and mac-site-isolation remove the [Timeout] markers for script-redirect-blocked.html and stylesheet-redirect-blocked.html, which were timing out due to this bug.

Premature CSP nonce evaluation during speculative preloading against an empty policy set, causing a permanent policy-check bypass flag to be set on the resource loader.

Content Security Policy (CSP) can be delivered via a <meta http-equiv="Content-Security-Policy"> tag in addition to HTTP headers. When delivered via meta tag, the policy only takes effect once the HTML parser reaches and processes that tag. WebKit's preload scanner (HTMLResourcePreloader) runs ahead of the main HTML parser to discover and speculatively fetch subresources (scripts, stylesheets, images) earlier, reducing page load time — this is a performance optimization that creates a timing window where resources may be fetched before security policies are installed.

CSP nonce-based allowlisting lets a resource bypass CSP checks if it carries a nonce attribute matching one declared in the policy. allowScriptWithNonce() and allowStyleWithNonce() return true if the nonce matches any policy's allowlist — or if no policies exist at all (vacuously true). ContentSecurityPolicyImposition::SkipPolicyCheck is a flag on ResourceLoaderOptions that tells the resource loading infrastructure to skip all CSP checks for that resource, including checks on redirect targets. Once set, this flag persists on the SubresourceLoader for the resource's entire lifetime.

The root cause is a semantic mismatch in the interpretation of allowScriptWithNonce(""). Before the fix, PreloadRequest::resourceRequest() unconditionally called allowScriptWithNonce(m_nonceAttribute) even when m_nonceAttribute was empty. The preload scanner runs ahead of the HTML parser, so when parser-blocking scripts appear before a <meta http-equiv="Content-Security-Policy"> tag, the preload scanner fetches resources declared after the meta tag before the CSP policy has been parsed. At this point, document.contentSecurityPolicy() has no policies. Calling allowScriptWithNonce("") against an empty policy set returns true — vacuously, because no policy exists to deny it (if the function behaves as the fix pattern strongly implies). HTMLResourcePreloader interprets this true as a valid nonce match and sets SkipPolicyCheck. This flag persists across redirects on the SubresourceLoader (as evidenced by the test names script-redirect-blocked and stylesheet-redirect-blocked). When the meta CSP tag is later parsed and the resource follows a redirect to a URL that violates the CSP, the redirect check is skipped.

🔒

Explores how the speculative preloader's interaction with CSP timing creates a permanent policy bypass, and the conditions under which this is exploitable

Subscribe to read more

🔒

Multiple audit patterns identified around early security-flag decisions and vacuous policy checks, applicable across WebKit's resource loading infrastructure

Subscribe to read more