← 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.

On a page protected by Content-Security-Policy: object-src;, an attacker with an HTML injection sink would inject <object type="..."></object> or <embed type="..."> with no data/src. Pre-fix, the empty-URL path returned true, and allowObjectFromSource() permitted the plugin instantiation. The plugin selected by the type attribute would then run despite the deployed deny-all policy.

The fix's strategy — substitute the document's own URL when the resource URL is empty — is the cleaner pattern: it lets the same matcher handle no-URL cases without bespoke logic, because the document URL naturally fails 'none' and empty lists while succeeding 'self'/wildcards.

This vulnerability weakens the CSP object-src defense-in-depth boundary. The HTML/CSP spec assumes that object-src; is equivalent to object-src 'none' — both should forbid any plugin instantiation. Before the fix that invariant was violated for the no-URL plugin path, so a page that injected <embed type="..."> or <object type="..."></object> into a document protected by object-src; could still cause the browser to instantiate the default plugin for the type, sidestepping a policy the author intended as deny-all. An attacker who already had an HTML injection foothold could broaden plugin attack surface against the page despite the deployed CSP — undermining a mitigation often deployed specifically to neuter object/embed-based exploitation.

Note: A few claims (m_sourceList.isNone() returning true only for the literal 'none' token, the specific call sites consuming allowObjectFromSource, and the §6.7.2.7 spec citation) are inferred from the patch comment and surrounding code rather than visible in the diff. The bypass mechanism — empty source list reaching the permissive branch — is directly demonstrated by the test cases the patch adds.