All posts

The data scheme in Content Security Policy

CentralCSP Team ·

Last update:

The data: scheme lets a URL carry its own content inline, so data:image/png;base64,... is a whole image with no server to fetch it from. In a Content Security Policy (CSP) you can allow that scheme per directive. For images and fonts it is convenient and common. For scripts it hands an attacker a way to run code with no external host at all.

This post shows where data: belongs in a policy, where it does not, and why the difference matters.

What the data scheme looks like in CSP

data: is a scheme source. The grammar is the scheme name followed by a colon, so you write the bare data: with a trailing colon and no quotes, unlike keyword sources such as 'self'.

The common, reasonable use is inline images and fonts:

Content-Security-Policy: img-src 'self' data:; font-src 'self' data:

That allows img-src and font-src to load resources from your own origin and from inline data URIs, which is how base64 icons, tiny placeholder images, and embedded fonts get rendered.

Each directive is independent. There is no data: on a directive unless you write it there, and it does not arrive through a fallback or a wildcard (more on the wildcard below).

Why data is safe for images but dangerous for scripts

A data: image cannot run code. It is bytes the browser decodes and paints. A data: script is different: the URL carries the script body, so the entire payload travels inside the source expression.

If script-src allows the scheme, an attacker who can inject a single <script> tag does not need to host anything. The code goes straight in the URL:

<script src="data:text/javascript,alert(document.domain)"></script>
<script src="data:;base64,YWxlcnQoMSk="></script>

Current Chrome, Firefox, and Safari execute external data: scripts, so this is a working execution vector today, not a legacy quirk. The risk write-ups frame data: in script-src as a CSP bypass; the CSP specification itself only defines the scheme matching and does not single it out as an XSS warning, so treat the danger as a hardening finding rather than a spec rule.

One distinction keeps this honest. Browsers block top-level navigation to a data: URL, an older anti-phishing change, but that does not block data: subresources like <script src="data:..."> or <img src="data:...">. The script loads as a subresource inside the page's own origin, so the navigation block does not apply.

The wildcard does not cover data

A natural assumption is that script-src * allows everything, including data:. It does not. In CSP Level 3, a single * matches a URL only when the URL uses an HTTP or HTTPS scheme (plus the same scheme as the protected document). The data:, blob:, and filesystem: schemes are excluded and must be listed explicitly.

Content-Security-Policy: script-src *

That policy does not permit data: scripts. You would have to add data: yourself for them to run, which is exactly why you should not.

This exclusion is a deliberate security fix. Browsers later restricted data: matching after allowing it through a wildcard proved to be a real bypass, and the behavior is consistent across current browsers.

Where to allow data, and where not to

Decide per directive, based on whether the directive can lead to code running:

  • img-src, font-src, and media-src: allowing data: is legitimate and widely used for inline base64 assets.
  • script-src: do not allow data:. It is an XSS execution vector as shown above.
  • object-src and frame-src: also risky, since data: plugins or frames can carry active content.
  • style-src: discouraged. An attacker-controlled data: stylesheet loads as a subresource and applies, which is a real CSS-based injection and data-exfiltration vector (though not code execution).
  • default-src: especially bad. When no script-src is present, default-src governs scripts, so default-src data: effectively permits data: scripts.

A large share of sites historically allowed data: under script-src, frame-src, or object-src (or default-src when script-src was absent), which is why this stays a common finding.

The safer alternative for scripts

The fix is not to find a careful way to allow data: in script-src. It is to stop allowing scripts by scheme or host at all and to allow them by nonce or hash instead, with 'strict-dynamic'.

Chrome and Google guidance is explicit that host and scheme allowlists give little protection, and that a nonce or hash plus 'strict-dynamic' is the model to use. A minimal strict policy looks like this:

Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic';
    object-src 'none';
    base-uri 'none'

With a nonce in place, the browser trusts only the scripts carrying that nonce and the scripts those trusted scripts load. A data: script with no matching nonce is rejected, so the bypass closes even if an attacker injects a tag. For the wider case against opening the policy this way, see why removing 'unsafe-inline' matters and the broader walkthrough in how to build a strong CSP.

If you need a few inline base64 images or fonts, keep data: scoped to img-src and font-src and leave it out of everything that touches script or style.

Find data in your own policy

Roll out changes the safe way: put a candidate policy on the Content-Security-Policy-Report-Only header first, watch the reports, and only enforce once they are quiet. To see where your live policy stands today, run it through the CSP evaluator, which flags data: in script-src and other weak sources so you know what to tighten before they become a way in.

The evaluator reads the policy you already ship. What it cannot tell you is which data: URIs your pages actually load, because that only shows up at runtime. CentralCSP collects the csp-violation reports from real visits and groups them by blocked URI, so when you remove data: from script-src you can see exactly which inline payloads stop loading, and whether any of them were yours.