# The blob scheme in Content Security Policy (/en/blog/csp-blob-scheme)





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](/en/blog/get-started-with-csp) covers your first policy before you tune the edge cases.

## What is a blob URL? [#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://example.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.

```html
<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 [#which-directive-governs-blob]

The directive depends on what the blob is used for.

For a worker, the [`worker-src`](/en/docs/web-security/policies/content-security-policy/directives/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`](/en/docs/web-security/policies/content-security-policy/directives/child-src), then [`script-src`](/en/docs/web-security/policies/content-security-policy/directives/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:

```http
Content-Security-Policy: media-src 'self' blob:
```

```http
Content-Security-Policy: img-src 'self' blob:
```

## Why self and the wildcard do not cover 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](/en/docs/web-security/policies/content-security-policy/values/csp-keywords). 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](/en/docs/web-security/policies/content-security-policy/values/csp-scheme-source):

```http
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](https://developer.chrome.com/docs) versions once treated `'self'` as covering same-origin `blob:` scripts, which contradicted the spec and [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox) and [WebKit](https://webkit.org/). Current Chrome requires `blob:` explicitly, so you can rely on the same rule everywhere.)

## What a blocked blob looks like in reports [#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](/en/blog/csp-violation-report-fields) 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](/en/blog/get-started-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`.

<img alt="Violation rows whose blocked origin is a scheme rather than a host" src="__img0" width="1359" height="208" />

## The script risk [#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.

```html
<!-- 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 [#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](/en/docs/web-security/policies/content-security-policy/values/csp-hashes-nonce) 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:

```html
<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:

```http
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 [#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](/en/docs/web-security/policies/content-security-policy/report-only) 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](/tools/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 [#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](/en/blog/how-to-build-a-strong-csp) covers nonces, hashes, and removing the keywords that weaken the policy.

## Related [#related]

* [The data scheme in Content Security Policy](/en/blog/csp-data-scheme)
* [How to build a strong CSP, step by step](/en/blog/how-to-build-a-strong-csp)
