# CSP Checker

> See what a Content-Security-Policy blocks in an HTML page, and the exact hashes that would allow its inline scripts and styles.

Category: Text & Data · Page: https://util.caruana.me/tools/csp/

## API, MCP and A2A

### Check HTML against a Content-Security-Policy (`csp_check`)

`GET|POST https://util.caruana.me/api/csp/check` · MCP tool `csp_check` · A2A skill `csp_check` · [reference](https://util.caruana.me/reference/csp_check/)

Parses `policy` (a Content-Security-Policy header value; commas separate several policies, which must all allow) and any `<meta http-equiv="Content-Security-Policy">` in `html`, then resolves each directive's fallback (CSP Level 3: `script-src-elem` → `script-src` → `default-src`, `frame-src` → `child-src` → `default-src`, …). Given `html`, it lists every inline script, inline `<style>`, `style` attribute, event-handler attribute, `javascript:` URL and external resource (scripts, stylesheets, images and srcset, fonts and other preloads, media, frames, objects, manifests, form actions, `<base>`) with `allowed`, the matching source or the reason it is blocked, the `sha256` hash source of inline content, and a fix. `hashes_to_add` collects, per directive, the hash sources (and `'unsafe-hashes'` for attributes) that would allow everything inline that is blocked. Without `html`, it lints the policy alone. Matching follows CSP Level 3: `'self'` (with http→https upgrades), scheme and host sources with `*.` wildcards, ports and paths, nonces (with the nonceable check), hashes (base64url accepted), `'unsafe-inline'` (ignored when a hash or nonce is present), `'unsafe-hashes'`, `'strict-dynamic'` and integrity metadata. A `<meta>` policy governs only what comes after it. Nothing is fetched: `html` is the document as you pass it, so content scripts add at run time is not seen. Script elements whose `type` is not JavaScript (JSON, JSON-LD, templates) are data blocks that CSP never checks; they are counted in `summary.ignored_data_blocks`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `policy` | string (≤ 20000 chars) | no | The Content-Security-Policy header value. Optional when `html` contains a `<meta http-equiv="Content-Security-Policy">`; when both are given, both are enforced. |
| `html` | string (≤ 500000 chars) | no | The HTML document to check, as the server sends it (for example a built page). Optional: without it, only the policy is analysed. |
| `url` | string (≤ 2000 chars) | no | The page's own URL, used for `'self'` and to resolve relative URLs. Default: relative URLs count as same-origin and absolute ones as cross-origin. |

Privacy: The server receives the policy and the HTML, analyses them in memory inside the request, and returns the result. Nothing is fetched: the server makes no network requests, not even for the URLs in the HTML. Your input is not stored or logged; only the per-call metadata row (operation, time, status, sizes) is recorded. For unpublished pages use the page, which does the same work in your browser and sends nothing.


## User guide

The tool sits in a window that works like a desktop app: menus on the top row and the **full-screen** icon at its right (**Esc** leaves full screen). The page URL option described below is in **Settings → Options…**, an overlay. To keep it in view instead, choose **View → Inline config**; the page remembers that choice. **Help → Keyboard shortcuts…** lists the keys.

1. **Paste the policy** on the left: the value of your `Content-Security-Policy` header, for example `default-src 'self'; script-src 'self'`. Leave it empty if the HTML carries its own `<meta http-equiv="Content-Security-Policy">`.
2. **Paste the HTML** on the right: the page as your server sends it (view source, or a file from your build output), not what the browser's inspector shows after scripts have run. **Sample** loads a page with something blocked of each kind.
3. Read the verdict and the **Findings**: every inline script, `<style>`, `style=""` attribute, event handler (`onclick=…`), `javascript:` link and external resource, blocked ones first. Each says which directive applied, and either the source that allowed it or why it is blocked and how to fix it.
4. **Hashes to add** lists, per directive, the exact `'sha256-…'` sources that would allow everything inline that is blocked, ready to copy into your policy. Attributes also need `'unsafe-hashes'`, which is included when it applies.
5. Under each **Policy** you see its directives, which directive governs each kind of resource after fallback, and warnings about weaknesses (`'unsafe-inline'`, wildcards, no `object-src 'none'`, no `base-uri`, sources missing their quotes).

With only a policy, the page is a policy linter: it parses it, resolves the fallbacks and lists its warnings.

**Page URL (Settings).** `'self'` means "the page's own origin", so give the URL the page is served from to check absolute URLs against `'self'` correctly. Without it, relative URLs count as same-origin and absolute ones as cross-origin.

**For agents.** `csp_check` over the API, MCP or A2A takes `policy`, `html` and `url` and returns the same findings. A good use: after building a site with a strict policy, check each built page before deploying.

## Specification

The checker implements the parts of [Content Security Policy Level 3](https://www.w3.org/TR/CSP3/) that can be decided from the policy and the HTML alone.

**Parsing.** A header value may hold several policies separated by commas; each is enforced, so something is allowed only if every policy allows it (CSP3 §4.1). Within a policy, directives are separated by `;`, names are case-insensitive, and a repeated directive is ignored after its first occurrence (§2.2.1). A `<meta http-equiv="Content-Security-Policy">` policy is enforced too, but only for the elements after it, and it cannot set `frame-ancestors`, `report-uri` or `sandbox` (§3.3).

**Fallbacks** (§6.8.3). Each request or inline item has an effective directive; the first of its fallback list that the policy contains applies, and if none does it is unrestricted:

| Effective directive                                                             | Falls back to                            |
| ------------------------------------------------------------------------------- | ---------------------------------------- |
| `script-src-elem`                                                               | `script-src`, `default-src`              |
| `script-src-attr`                                                               | `script-src`, `default-src`              |
| `style-src-elem`                                                                | `style-src`, `default-src`               |
| `style-src-attr`                                                                | `style-src`, `default-src`               |
| `worker-src`                                                                    | `child-src`, `script-src`, `default-src` |
| `frame-src`                                                                     | `child-src`, `default-src`               |
| `img-src`, `font-src`, `media-src`, `connect-src`, `manifest-src`, `object-src` | `default-src`                            |
| `base-uri`, `form-action`, `frame-ancestors`                                    | nothing: unrestricted unless set         |

**What is checked, and against what.**

| Finding                                | From                                                                                                                                 | Directive                                              |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| `inline-script`                        | `<script>` without `src` whose type is JavaScript, `module`, `importmap` or `speculationrules`                                       | `script-src-elem`                                      |
| `inline-style`                         | `<style>`                                                                                                                            | `style-src-elem`                                       |
| `style-attribute`                      | any `style=""` attribute                                                                                                             | `style-src-attr`                                       |
| `event-handler`                        | any `on…=""` attribute                                                                                                               | `script-src-attr`                                      |
| `javascript-url`                       | `javascript:` in `a`/`area` `href`, `iframe`/`frame` `src`, `form` `action`, `formaction`                                            | `script-src-elem`                                      |
| `external-script`                      | `<script src>`, `<link rel=modulepreload>`, `<link rel=preload as=script>`                                                           | `script-src-elem` (`worker-src` for `as=worker`)       |
| `external-style`                       | `<link rel=stylesheet>`, `<link rel=preload as=style>`                                                                               | `style-src-elem`                                       |
| `image`                                | `<img src>` and each `srcset` URL, `<source srcset>`, `<input type=image>`, `<video poster>`, `<link rel=icon>`                      | `img-src`                                              |
| `font`, `connect`, `media`, `manifest` | `<link rel=preload as=font / fetch / audio / video / track>`, `<audio>`, `<video>`, `<source src>`, `<track>`, `<link rel=manifest>` | `font-src`, `connect-src`, `media-src`, `manifest-src` |
| `frame`, `object`                      | `<iframe src>`, `<frame src>`; `<object data>`, `<embed src>`                                                                        | `frame-src`; `object-src`                              |
| `form-action`, `base-uri`              | `<form action>`, `formaction`; `<base href>`                                                                                         | `form-action`; `base-uri`                              |

`<script>` elements with any other type (`application/json`, `application/ld+json`, `text/template`, …) are data blocks: browsers never run them and CSP never checks them (HTML "prepare the script element"). They are counted, not reported.

**Inline content** (§6.7.3.3). In order:

1. `'unsafe-inline'` allows it, unless the same list has a hash or a nonce, or (for scripts) `'strict-dynamic'`; then `'unsafe-inline'` is ignored.
2. For `<script>` and `<style>` elements, a `nonce` attribute equal to a `'nonce-…'` source allows it, if the element is _nonceable_ (no attribute name or value contains `<script` or `<style`, the HTML spec's dangling-markup check).
3. A `'sha256-…'`, `'sha384-…'` or `'sha512-…'` source equal to the hash of the content allows it. For `style` and event-handler attributes this applies only when the list also has `'unsafe-hashes'`. Hash values may be base64 or base64url.

`javascript:` URLs are allowed only by `'unsafe-inline'` here; hash-allowing them (with `'unsafe-hashes'`) is not evaluated.

**Hashes are computed as browsers compute them:** over the UTF-8 bytes of the element's text exactly as written between the tags, after the HTML parser's newline normalization (CR LF and lone CR become LF), and, for attributes, over the value after character references are decoded (`&quot;` becomes `"`). The `hash` field is always `sha256`; any of the three algorithms in the policy is matched.

**External resources** (§6.7.2). A nonce on `<script>` or `<link rel=stylesheet>` matching a `'nonce-…'` source allows the request. A script whose `integrity` hashes are all listed as hash sources is allowed. With `'strict-dynamic'`, host, scheme and `'self'` sources are ignored for scripts, so a script in the HTML needs a nonce or integrity hash; scripts that those scripts load later are trusted but cannot be seen here. Otherwise the URL is matched against each source:

- `*` matches http and https URLs and the page's own scheme, but not `data:`, `blob:` or `filesystem:`.
- `'self'` matches the page's origin, and the same host over https or wss from an http page.
- A scheme source such as `https:` matches that scheme; `http:` also matches `https:`, `ws:` matches `wss:`, `http:` and `https:`.
- A host source matches its host (`*.example.com` matches subdomains, not `example.com` itself), its scheme if given (with the same upgrades), its port (or the scheme's default port, with 80 upgrading to 443; `:*` matches any port), and its path: a path ending in `/` matches everything under it, any other path matches exactly.
- `'none'` matches nothing; listed with other sources it is ignored.

Relative URLs resolve against the page URL, or against a `<base href>` that `base-uri` allows.

**Not evaluated.** Nothing is fetched, so redirects, CSS `@import` and `url()`, workers created by scripts, `fetch()` calls, and anything scripts add to the page at run time are not seen. `frame-ancestors`, `sandbox`, `upgrade-insecure-requests`, `trusted-types` and reporting directives are parsed and listed but have nothing to check in a document. The tokenizer reads the document as the HTML parser does for the cases CSP depends on (comments, raw text in `<script>`, `<style>`, `<textarea>` and `<title>`, attribute quoting, the first of duplicate attributes), but it does not build a tree, so it cannot tell `<source>` in `<picture>` from `<source>` in `<video>` (`srcset` is treated as images, `src` as media) and does not handle the rare `<!--<script>` escape inside scripts.

**Limits.** API: `policy` at most 20,000 characters and `html` at most 500,000. The page has no limit.

**API.** `POST /api/csp/check` with `policy`, `html` and `url`. Something blocked is a 200 response; only a missing policy or an invalid `url` is an error. With `format=text`, the response is `allowed`, or `blocked: <n>` followed by one line per blocked finding.
