[2] CSP empty object-src source list treated as permissive for URL-less plugins
The object-src spelling that looks like deny-all was the permissive one.
Rated Medium — two policies the spec defines as identical produced opposite decisions, and the permissive one was the deny-all-looking spelling. No memory primitive; the cost is that a CSP deployed specifically to kill plugin instantiation didn't.
Content Security Policy lets a document declare, per resource type, which sources may be loaded; the object-src directive is the gate for <object> and <embed> plugin loads. The right-hand side of a directive is a source list — it may hold tokens like 'self' or 'none', scheme and host expressions, or nothing at all. CSP Level 3 specifies that an empty source list matches no URL, making object-src; semantically identical to object-src 'none', and both should forbid any plugin instantiation.
The angle: on a page hardened with object-src;, an attacker with an HTML injection sink can inject a <object type="..."> or <embed type="..."> carrying no data/src and still get the default plugin for that type instantiated.
When an
<object>or<embed>element has nodata/srcattribute, WebKit previously passed an empty URL to 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.Remove the special-case handling from §6.1.9 entirely. Instead, use the document's own URL as a fallback for source list matching when the element has no associated URL. The document URL will naturally fail to match empty source lists and
'none'(blocked), but will match'self'or wildcard (allowed).
Source/WebCore/page/csp/ContentSecurityPolicySourceListDirective.cpp
Source/WebCore/page/csp/ContentSecurityPolicy.cpp
LayoutTests/imported/w3c/web-platform-tests/content-security-policy/object-src/object-src-no-url-empty-source-list-blocked.html
Patch Details
The change removes the ShouldAllowEmptyURLIfSourceListIsNotNone special case from source-list matching and replaces it with a URL substitution at the caller. ContentSecurityPolicySourceListDirective::allows() now returns false unconditionally when the URL is empty, instead of consulting !m_sourceList.isNone(); the parameter disappears from the signature, from checkSource(), from violatedDirectiveForObjectSource(), and from the related allows() overload, and the two checkFrameAncestors() overloads drop the now-removed third argument. In ContentSecurityPolicy::allowObjectFromSource(), an empty incoming url — the <object>/<embed> with no data/src — is replaced by the document's own m_protectedURL, and that substituted URL is used for the scheme bypass check, for the reported blocked URL, and as the URL passed to allPoliciesAllow. Three new WPT tests assert that <object>/<embed> without a URL are blocked under both object-src 'none' and object-src;.
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.
Background
What CSP gates.
Content Security Policy is an HTTP-header- or <meta http-equiv>-delivered policy that lets a document restrict which sources may be loaded for each resource type. object-src governs <object>/<embed> plugin loads; allowObjectFromSource() is WebKit's gate for those loads and is called from HTMLPlugInElement, CachedResourceLoader, and PolicyChecker.
Source lists and their two "empty" spellings.
A source list is the right-hand side of a directive. It can hold tokens like 'self', 'none', or scheme/host expressions, or it can be empty. CSP Level 3 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, and an empty list returns false.
Plugin elements with no associated URL.
<object> and <embed> may instantiate a plugin with no associated URL: when the element has neither data nor src, only the type attribute selects the plugin. This case prompted a special clause in early CSP drafts (§6.1.9) that allowed such no-URL plugins unless 'none' was specified — a clause the cleaner Level 3 model no longer needs.
Analysis
The root cause is a logic error in the empty-URL fast path of source-list matching. Before the fix, ContentSecurityPolicySourceListDirective::allows() short-circuited on an empty URL with the predicate shouldAllowEmptyURLIfSourceListEmpty == Yes && !m_sourceList.isNone(), implementing the §6.1.9 special case cited in the now-removed comment: a plugin with no URL is blocked only by 'none', otherwise allowed.
That predicate conflates two distinct CSP states. m_sourceList.isNone() is true only for the literal 'none' token, so an empty source list evaluates it as false and the predicate becomes Yes && !false = true:
Policy isNone() empty-URL fast path Spec says
──────────────────── ──────── ───────────────────── ─────────
object-src 'none'; true Yes && !true = false block ✓
object-src; false Yes && !false = true block ✗ allowed
object-src 'self'; false Yes && !false = true allow ✓
The middle row is the bug: a policy of object-src; permitted an <object>/<embed> with no data/src to load its default plugin, even though the author wrote the policy to forbid all object sources. The claim that isNone() is true only for the literal token is the crux of the mechanism and is drawn from the removed predicate's own semantics; ContentSecurityPolicySourceList's implementation is not part of the supplied context.
The fix takes the special case out of the matcher entirely and moves the handling up one level. With allows() returning false for any empty URL, allowObjectFromSource() substitutes the document's own protected URL before matching. That substituted URL then flows through the ordinary matcher: it fails to match 'none' and fails to match an empty list (neither matches anything), but succeeds against 'self' or a wildcard — reproducing the intended §6.1.9 outcome for permissive policies without a bespoke branch.
Exploitation is a policy bypass, not a memory-safety issue. On a page protected by Content-Security-Policy: object-src;, an attacker with an HTML injection sink injects <object type="..."></object> or <embed type="..."> with no data/src attribute; before the fix the empty-URL path returned true and allowObjectFromSource() permitted the instantiation, so the plugin selected by the type attribute could run despite the deny-all policy. The result is broader plugin attack surface available to follow-on exploits within the WebContent process — not a sandbox escape, since it requires an existing injection vector in a CSP-protected page and only relaxes which plugins can be instantiated under that page's policy.
The discovery angle looks like WPT conformance work: the new tests land under imported/w3c/web-platform-tests/content-security-policy/object-src/ and assert behaviour explicitly required by CSP Level 3 §6.7.2.7, which fits either importing upstream WPT and observing WebKit failures, or a reviewer noticing that the §6.1.9 comment referenced an editor's draft that had since been superseded. The Level 3 §6.7.2.7 citation itself is relayed from the commit message.
This vulnerability weakens the CSP object-src defense-in-depth boundary. The spec assumes object-src; and object-src 'none' are interchangeable — both forbid any plugin instantiation — and before the fix that invariant was violated on the no-URL plugin path, undermining a mitigation often deployed specifically to neuter object/embed-based exploitation.
Insight: the bug is a fossil. CSP Level 1/2 carried a literal §6.1.9 special case for no-URL plugin elements that pre-dated the cleaner Level 3 model where an empty source list is uniformly deny-all; carrying that legacy case into the matcher created a state where two semantically equivalent policies produced different decisions. The fix's strategy — substitute the document's own URL when the resource URL is empty — is worth borrowing: 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' and wildcards.
Audit directions
- Security-policy matchers carrying legacy spec special cases for "no associated URL" or "opaque origin" inputs. Audit other source-list consumers (
violatedDirectiveForFrame,violatedDirectiveForMedia,violatedDirectiveForConnectSource, etc.) and theContentSecurityPolicySourceList::matchespath for branches keyed onurl.isEmpty(),protocolIsAbout(), orisOpaque()that may diverge between an explicit'none'token and an empty source list. Start inSource/WebCore/page/csp/ContentSecurityPolicySourceList.cppand grep forisNone()callers. - Predicates of the shape
flag == Yes && !state.isX()that conflate "state X is set" with "state X is non-trivial". Review CSP and other policy code for similar two-condition gates where the negation operates on an enum or flag whosefalsevalue covers multiple semantically distinct cases —'none'literal vs. empty list, default vs. unset. Grep WebCore for!.*isNone()and!.*isEmpty()inside boolean conditions guarding security decisions. In code review, a negatedisX()inside a security gate deserves a comment naming every state that makes it false. - Callers that synthesise an empty
URLto mean "no associated resource". Audit consumers ofallowObjectFromSourceand the otherallowXFromSourceentry points inContentSecurityPolicy.h, and confirm each such caller now produces a sensible decision under the document-URL fallback (or that an empty URL is rejected outright). Start withHTMLPlugInElement::allowedToLoadFrameURLand theCachedResourceLoader::canRequestpaths flagged withLoadedFromPluginElement::Yes. - Delivery-mode and cross-directive symmetry. Verify parity between meta-delivered and header-delivered CSPs by re-running the new WPT cases under both delivery modes, and check that
frame-src,media-src, andconnect-srcproduce equivalent block decisions for'none'and empty source lists when the request URL is missing orabout:blank.