← All issues

[2] CSP object-src empty-source-list bypass for no-URL plugin elements

Severity: Medium | Component: WebCore Content Security Policy enforcement | c6228ab

Rated Medium because the diff fixes a CSP object-src policy bypass where an empty source list was treated as permissive instead of deny-all for no-URL <object>/<embed> instantiations. The bug expands plugin attack surface against pages relying on the deployed policy but produces no direct memory primitive; exploitation requires an existing HTML injection foothold, and the impact is gated to CSPs that use the empty-source-list form rather than 'none'.

When an <object> or <embed> element has no data/src attribute, WebKit previously passed an empty URL into the CSP check with special-case logic that only blocked for the literal 'none' keyword. An empty source list (object-src;) was incorrectly allowed despite being equivalent to 'none' per CSP Level 3 §6.7.2.7. The fix removes the §6.1.9 special case entirely and substitutes the document's own URL as a fallback for source-list matching when the element has no associated URL — the document URL naturally fails to match empty source lists and 'none' (blocked), but matches 'self' or wildcards (allowed). Three new WPT tests assert that <object>/<embed> without a URL are blocked under both object-src 'none' and an empty source list.

Source/WebCore/page/csp/ContentSecurityPolicySourceListDirective.cpp

-bool ContentSecurityPolicySourceListDirective::allows(const URL& url, bool didReceiveRedirectResponse, ShouldAllowEmptyURLIfSourceListIsNotNone shouldAllowEmptyURLIfSourceListEmpty)
+bool ContentSecurityPolicySourceListDirective::allows(const URL& url, bool didReceiveRedirectResponse)
{
if (url.isEmpty())
- return shouldAllowEmptyURLIfSourceListEmpty == ShouldAllowEmptyURLIfSourceListIsNotNone::Yes && !m_sourceList.isNone();
+ return false;
return m_sourceList.matches(url, didReceiveRedirectResponse);
}

Source/WebCore/page/csp/ContentSecurityPolicy.cpp

- if (m_policies.isEmpty() || LegacySchemeRegistry::schemeShouldBypassContentSecurityPolicy(url.protocol()))
+ const auto& urlToCheck = url.isEmpty() ? m_protectedURL : url;
+ if (m_policies.isEmpty() || LegacySchemeRegistry::schemeShouldBypassContentSecurityPolicy(urlToCheck.protocol()))
return true;
- // ... 'MUST be blocked if object-src's value is 'none', but will otherwise be allowed' ...
String sourceURL;
- const auto& blockedURL = !preRedirectURL.isNull() ? preRedirectURL : url;
+ const auto& blockedURL = !preRedirectURL.isNull() ? preRedirectURL : urlToCheck;
...
- return allPoliciesAllow(handleViolatedDirective, &ContentSecurityPolicyDirectiveList::violatedDirectiveForObjectSource, url, redirectResponseReceived == RedirectResponseReceived::Yes, ContentSecurityPolicySourceListDirective::ShouldAllowEmptyURLIfSourceListIsNotNone::Yes);
+ return allPoliciesAllow(handleViolatedDirective, &ContentSecurityPolicyDirectiveList::violatedDirectiveForObjectSource, urlToCheck, redirectResponseReceived == RedirectResponseReceived::Yes);

LayoutTests/imported/w3c/web-platform-tests/content-security-policy/object-src/object-src-no-url-empty-source-list-blocked.html

+<meta http-equiv="Content-Security-Policy" content="object-src; script-src 'self' 'unsafe-inline';">
+<object type="text/html"></object>

The patch removes the ShouldAllowEmptyURLIfSourceListIsNotNone parameter from ContentSecurityPolicySourceListDirective::allows, which now returns false unconditionally for empty URLs. In ContentSecurityPolicy::allowObjectFromSource, when the incoming url is empty (an <object>/<embed> with no data/src), the code substitutes m_protectedURL (the document's own URL) before invoking the policy match. The parameter is also removed from checkSource(), violatedDirectiveForObjectSource(), and the related allows() overload signature. Both checkFrameAncestors() overloads drop the now-removed third argument.

Conflation of two CSP states ('none' keyword vs. an empty source list) in the empty-URL fast path of source-list matching, causing an empty source list to behave permissively rather than as a deny-all.

Content Security Policy is an HTTP/meta-delivered policy that lets a document restrict which sources may be loaded for various resource types. The object-src directive controls <object>/<embed> plugin loads. A source list is the right-hand side of a directive: it can hold tokens like 'self', 'none', scheme/host expressions, or be empty. The CSP Level 3 spec specifies that an empty source list matches no URL, behaving identically to 'none'.

WebKit models a source list with ContentSecurityPolicySourceList; isNone() returns true only for the literal 'none' token, while an empty list returns false. <object>/<embed> may instantiate a plugin with no associated URL — when the element has neither data nor src, only the type attribute selects the plugin — which historically prompted a special clause in early CSP drafts that allowed such no-URL plugins unless 'none' was specified.

The pre-fix predicate inside allows() was shouldAllowEmptyURLIfSourceListEmpty == Yes && !m_sourceList.isNone(). The intent (cited in the now-removed comment) was the §6.1.9 special case: a plugin with no URL must be blocked only by 'none', otherwise allowed. But the predicate conflates two distinct CSP states: (a) the explicit 'none' keyword and (b) an empty source list (object-src;). Per CSP Level 3 §6.7.2.7 these are semantically equivalent — both match no URL — yet m_sourceList.isNone() is true only for the literal 'none' token. So a policy of object-src; produced Yes && !false = true, allowing a no-URL <object>/<embed> to load its default plugin even though the deployed policy was meant to forbid all object sources.

🔒

The policy-enforcement implications of conflating two CSP states in a single matcher predicate, and the boundary this weakens when the policy is deployed as a deny-all.

Subscribe to read more

🔒

Multiple reusable audit patterns identified across CSP source-list matching and similar two-condition policy gates, with concrete starting points for variant discovery.

Subscribe to read more