All posts

The blob scheme in Content Security Policy

CentralCSP Team ·

Last update:

If your page creates a Web Worker or plays media from a blob: URL, Content Security Policy (CSP) will block it unless you list blob: on the right directive. CSP is an HTTP response header that tells the browser which resources a page may load. The catch most people hit: the 'self' keyword and the * wildcard do not match blob:. You have to allow it by name.

The short version: blob: is a scheme source, like https: or data:. To allow a worker built from a blob you write worker-src 'self' blob:; to allow a blob script you write script-src 'self' blob:. But allowing blob: in a script context lets the page run code it builds at runtime, which is close to handing back 'unsafe-eval'. So allow it where you must (workers, media), and keep it out of script directives where you can.

New to this header? Get started with Content Security Policy covers your first policy before you tune the edge cases.

What is a blob URL?

A blob: URL points to a Blob or File object held in browser memory. JavaScript creates one at runtime with URL.createObjectURL(), and the result looks like blob:https://api-next.centralcsp.com/9d3a.... The URL is opaque and short-lived, and it lives only in that page. You cannot host it, allowlist it by host, or pin it with a hash.

Pages use blob URLs for legitimate work: generating a file for download, streaming recorded audio or video, and spinning up a Web Worker from code assembled on the fly. Here is the worker case, which is the one CSP trips on most often.

<script>
  // Worker code assembled at runtime, then run from a blob: URL
  const workerCode = `
    self.onmessage = (event) => {
      const sum = event.data.reduce((acc, value) => acc + value, 0);
      postMessage(sum);
    };
  `;
  const workerBlob = new Blob([workerCode], { type: "application/javascript" });
  const workerUrl = URL.createObjectURL(workerBlob);

  const worker = new Worker(workerUrl);
  worker.onmessage = (event) => console.log("sum:", event.data);
  worker.postMessage([1, 2, 3, 4, 5]);
</script>

With a strict policy in place, that new Worker(workerUrl) call fails until the policy permits blob:.

Which directive governs blob

The directive depends on what the blob is used for.

For a worker, the worker-src directive applies to Worker, SharedWorker, and ServiceWorker scripts. If worker-src is absent, the browser falls back down a chain: worker-src, then child-src, then script-src, then default-src. So a worker built from URL.createObjectURL() is checked against whichever of those is present first. If your only script directive is script-src 'self' with no worker-src, the worker is checked against script-src, and the blob: is blocked.

A blocked worker is not a soft failure. The browser treats the refused request as a fatal network error, so the worker simply never runs.

For a script loaded from a blob (a <script src="blob:..."> or any script-typed blob fetch), the governing directive is script-src, falling back to default-src.

Media and images follow the same scheme-source pattern on their own directive:

Content-Security-Policy: media-src 'self' blob:
Content-Security-Policy: img-src 'self' blob:

Why self and the wildcard do not cover blob

This is the part that surprises people. A same-origin blob: URL is not matched by the 'self' keyword. Even though the blob shares your origin, you still have to list blob: explicitly.

The * wildcard does not help either. The CSP source-list matching algorithm excludes blob:, data:, and filesystem: from a bare *. So default-src * does not permit blob:. These schemes are treated specially because they are local schemes that produce content from the page itself rather than from a network host.

That means a policy like script-src 'self' or even script-src * will block a blob worker or a blob script. To allow it, name the scheme as a scheme source:

Content-Security-Policy: worker-src 'self' blob:

Note that blob: is written unquoted, with the trailing colon. It is a scheme source, not a keyword, so it never takes the single quotes that 'self' or 'unsafe-inline' use.

All major browsers behave this way today. (Older Chrome versions once treated 'self' as covering same-origin blob: scripts, which contradicted the spec and Firefox and WebKit. Current Chrome requires blob: explicitly, so you can rely on the same rule everywhere.)

What a blocked blob looks like in reports

A refused blob: resource produces a standard CSP violation report, no special shape. The report's effectiveDirective reflects the directive that blocked it, for example worker-src or script-src. The blocked URL is typically reported as the scheme blob rather than the full opaque URL, because the browser sanitizes blocked URIs. The fields in a CSP violation report cover what each of these values means. The exact blockedURL string a browser reports for a blob: resource can vary, so key your handling on the effectiveDirective rather than the URL.

If you are not collecting these reports yet, get started with CSP reporting walks through wiring a report-to endpoint so blocked blobs show up instead of failing silently. A reporting endpoint looks like https://<Endpoint-ID>.report.centralcsp.com.

Violation rows whose blocked origin is a scheme rather than a host

The script risk

A page can assemble arbitrary code into a Blob and run it as a worker or a script through its blob URL. So allowing blob: on script-src or worker-src lets the page execute code it constructs at runtime. In practice that weakens the policy in a way comparable to 'unsafe-eval': if an attacker gains a foothold (through XSS or a compromised dependency), they can build a malicious blob and load it as a script, and the policy waves it through because the source is blob:, not because anyone reviewed the content.

<!-- If script-src allows blob:, this runs. Any script on the page could do it. -->
<script>
  const code = "console.log('arbitrary code via blob:', document.domain)";
  const blob = new Blob([code], { type: "text/javascript" });
  const s = document.createElement("script");
  s.src = URL.createObjectURL(blob);
  document.body.appendChild(s);
</script>

Blob content is generated at runtime, so it is also hard to audit. There is no static file to review and no host or hash to pin it to. That is the trade you accept when blob: sits in a script directive.

How to keep blob out of script contexts

The goal is to allow blob: only where it cannot execute code, and to serve scripts and workers from real URLs you control.

  1. Serve worker scripts from a static path instead of a blob. A file at /static/workers/task.js can be allowlisted by host, and it survives a tighter policy.
  2. Allow your own inline scripts with a nonce or a hash rather than a scheme. The nonce and hash values let you approve specific scripts by identity, which blob: cannot do.
  3. Keep blob: to non-executing directives like media-src or img-src when your app genuinely needs it for playback or image display.

A worker loaded from a static URL needs no blob at all:

<script>
  const worker = new Worker("/static/workers/task.js");
  worker.postMessage([1, 2, 3, 4, 5]);
</script>

The resulting policy keeps script and worker execution to your origin and allows blob: only for media:

Content-Security-Policy: default-src 'self';
    script-src 'self';
    worker-src 'self';
    media-src 'self' blob:

If you do need a blob worker and cannot move it to a static file, scope the allowance tightly: put blob: on worker-src only, so it never widens what script-src accepts.

Roll it out with Report-Only first

Before you enforce a policy that drops blob: from script and worker directives, run it in observe mode so you can see what would break. The Content-Security-Policy-Report-Only header reports violations without blocking anything. Watch for blocked blob workers, fix the legitimate ones by serving the worker from a static URL, then switch to enforce mode.

When you are auditing what your site already allows, our CSP scanner shows the live policy on a page and flags where blob: sits in a risky directive, so you can tighten it before an attacker finds it.

Quick reference

  • blob: is a scheme source. Write it unquoted, with the colon: worker-src 'self' blob:.
  • Neither 'self' nor * matches blob:. Allow it by name.
  • Workers resolve worker-src, then child-src, then script-src, then default-src. A blocked worker is a fatal network error.
  • Scripts resolve script-src, then default-src.
  • Allowing blob: in a script directive is close to 'unsafe-eval'. Keep it out of script-src and worker-src where you can; serve workers from static URLs instead.

Want to harden the rest of the header next? How to build a strong CSP covers nonces, hashes, and removing the keywords that weaken the policy.