/api/csp/checkMCP tool csp_checkParses 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.
Browser version (runs locally): CSP Checker.
Try it
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
policy | string (≤ 20000 chars) | — | 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) | — | 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) | — | 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. |
Also accepted on every endpoint: format (json | text). Parameters may come from the query string, a JSON object body, an urlencoded or multipart form, or (for input) a text/plain body.
Global parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
format | json | text | json | json returns the documented object. text returns only the primary value as text/plain (see "Text output"). |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
origin | string | null | The page origin used for 'self', or null when url was not given. |
policies | array of object | The policies enforced: the header ones first, then each <meta> policy. |
policies[].source | "header" | "meta" | Where the policy came from. |
policies[].line | integer | null | Line of the <meta> element; null for the header. |
policies[].text | string | The policy as written. |
policies[].directives | array of object | Directives in order; a repeated directive is ignored (see warnings). |
policies[].directives[].name | string | Directive name, lower-cased. |
policies[].directives[].sources | array of string | Its source list, as written. |
policies[].effective | map of string | null | For each effective directive (script-src-elem, img-src, …), the directive that governs it. |
policies[].warnings | array of string | Unknown or repeated directives, sources missing quotes, 'unsafe-inline' or wildcards in script lists, missing object-src or base-uri, directives a <meta> policy cannot set. |
findings | array of object | In document order. |
findings[].kind | "inline-script" | "inline-style" | "style-attribute" | "event-handler" | "javascript-url" | "external-script" | "external-style" | "image" | "font" | "media" | "frame" | "object" | "manifest" | "connect" | "form-action" | "base-uri" | What was found. |
findings[].line | integer | Line in the HTML (1-based). |
findings[].snippet | string | The markup, shortened to 120 characters. |
findings[].directive | string | The effective directive that governs it, e.g. script-src-elem. |
findings[].allowed | boolean | True when every policy allows it. |
findings[].by | string | null | The source that allowed it (per policy when there are several); (no directive) when nothing restricts it. |
findings[].reason | string | null | Why it is blocked, naming the directive that applied. |
findings[].url | string | null | The resolved URL of an external resource. |
findings[].hash | string | null | The 'sha256-…' hash source of the inline content (script or style text, attribute value), computed as browsers do. |
findings[].fix | string | null | How to allow it, when it is blocked. |
hashes_to_add | array of object | Hash sources that would allow every blocked inline script, style, style attribute and event handler. |
hashes_to_add[].directive | string | The directive to add them to (the one that blocked them). |
hashes_to_add[].sources | array of string | Hash sources, in document order. |
hashes_to_add[].note | string | What adding them changes. |
summary | object | |
summary.total | integer | Findings. |
summary.allowed | integer | Findings the policies allow. |
summary.blocked | integer | Findings a policy blocks. |
summary.ignored_data_blocks | integer | Script elements with a non-JavaScript type (JSON, JSON-LD, …), which CSP never checks. |
warnings | array of string | Unclosed raw-text elements, invalid URLs, content before a <meta> policy. |
Text output (format=text, text/plain): allowed or blocked: <n>, then one line per blocked finding: <line> <kind> <directive>: <reason>.
Errors
Errors are JSON: {"error": "<message>", "code": "<code>", "param": "<name>"?}.
| Status | Code | When |
|---|---|---|
400 | invalid_parameter | Neither policy nor a <meta> policy in html was given, url is not an absolute http(s) URL, or a parameter exceeds its length limit. |
400 | missing_parameter | A required parameter is absent. |
400 | unknown_parameter | A parameter name is not in the table above (typos are rejected, not ignored). |
413 | payload_too_large | The request body exceeds 1 MiB. |
405 | method_not_allowed | A method other than GET, POST or OPTIONS. |
Behaviour
- Idempotent: the same arguments always return the same result.
- Requires a client ID and secret (authentication). Each call is recorded as one metadata row against your client ID; inputs and outputs are never stored or logged. Responses carry
cache-control: no-store. - CORS: any origin may call this endpoint from a browser, using your credentials.
Examples
Responses below were produced by running each request through this code at build time. Long strings are shortened.
An inline script, a style attribute and an external script
curl -X POST 'https://util.caruana.me/api/csp/check' \
-H 'content-type: application/json' \
-d '{"policy":"default-src '\''none'\''; script-src '\''self'\''; style-src '\''self'\''","html":"<script>init()</script><p style=\"color:red\">x</p><script src=\"/app.js\"></script>"}'Response 200
{
"origin": null,
"policies": [
{
"source": "header",
"line": null,
"text": "default-src 'none'; script-src 'self'; style-src 'self'",
"directives": [
{
"name": "default-src",
"sources": [
"'none'"
]
},
{
"name": "script-src",
"sources": [
"'self'"
]
},
{
"name": "style-src",
"sources": [
"'self'"
]
}
],
"effective": {
"script-src-elem": "script-src",
"script-src-attr": "script-src",
"style-src-elem": "style-src",
"style-src-attr": "style-src",
"worker-src": "script-src",
"frame-src": "default-src",
"connect-src": "default-src",
"font-src": "default-src",
"img-src": "default-src",
"manifest-src": "default-src",
"media-src": "default-src",
"object-src": "default-src",
"base-uri": null,
"form-action": null,
"frame-ancestors": null
},
"warnings": [
"No base-uri: an injected <base> can redirect relative script URLs. base-uri 'none' or 'self' closes that."
]
}
],
"findings": [
{
"kind": "inline-script",
"line": 1,
"snippet": "<script>init()",
"directive": "script-src-elem",
"url": null,
"hash": "'sha256-w4ujnOpjBoH2vcasx+reJRUwYivG8Q3afx/XevGJod8='",
"fix": "Move it to an external file, or add 'sha256-w4ujnOpjBoH2vcasx+reJRUwYivG8Q3afx/XevGJod8=' to script-src.",
"allowed": false,
"by": null,
"reason": "script-src: no 'unsafe-inline', no matching nonce or hash"
},
{
"kind": "style-attribute",
"line": 1,
"snippet": "<p style=\"color:red\">",
"directive": "style-src-attr",
"url": null,
"hash": "'sha256-8f935d27GvUutRyY9yWScUMiFUk4WTdZURISiYfPOeQ='",
"fix": "Move the style attribute into an external stylesheet (a class), or add 'unsafe-hashes' 'sha256-8f935d27GvUutRyY9yWScUMiFUk4WTdZURISiYfPOeQ=' to style-src.",
"allowed": false,
"by": null,
"reason": "style-src: no 'unsafe-inline', no matching hash"
},
{
"kind": "external-script",
"line": 1,
"snippet": "<script src=\"/app.js\">",
"directive": "script-src-elem",
"url": "/app.js",
"hash": null,
"fix": null,
"allowed": true,
"by": "'self'",
"reason": null
}
],
"hashes_to_add": [
{
"directive": "script-src",
"sources": [
"'sha256-w4ujnOpjBoH2vcasx+reJRUwYivG8Q3afx/XevGJod8='"
],
"note": "Adding a hash or nonce makes browsers ignore 'unsafe-inline' in the same directive."
},
{
"directive": "style-src",
"sources": [
"'unsafe-hashes'",
"'sha256-8f935d27GvUutRyY9yWScUMiFUk4WTdZURISiYfPOeQ='"
],
"note": "Includes 'unsafe-hashes', which lets these hashes allow event handlers and style attributes. Adding a hash or nonce make… (56 more characters)"
}
],
"summary": {
"total": 3,
"allowed": 1,
"blocked": 2,
"ignored_data_blocks": 0
},
"warnings": []
}Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "csp_check",
"arguments": {
"policy": "default-src 'none'; script-src 'self'; style-src 'self'",
"html": "<script>init()</script><p style=\"color:red\">x</p><script src=\"/app.js\"></script>"
}
}
}Nonces, a CDN and a <meta> policy
curl -X POST 'https://util.caruana.me/api/csp/check' \
-H 'content-type: application/json' \
-d '{"html":"<!doctype html>\n<head>\n<meta http-equiv=\"Content-Security-Policy\" content=\"script-src '\''nonce-r4nd0m'\'' https://cdn.example.com; img-src '\''self'\'' data:\">\n<script nonce=\"r4nd0m\">start()</script>\n<script src=\"https://cdn.example.com/lib.js\"></script>\n<script src=\"https://evil.example.net/x.js\"></script>\n</head>\n<body><img src=\"/logo.png\"><img src=\"https://img.example.org/a.png\"><button onclick=\"go()\">Go</button></body>","url":"https://www.example.com/"}'Response 200
{
"origin": "https://www.example.com",
"policies": [
{
"source": "meta",
"line": 3,
"text": "script-src 'nonce-r4nd0m' https://cdn.example.com; img-src 'self' data:",
"directives": [
{
"name": "script-src",
"sources": [
"'nonce-r4nd0m'",
"https://cdn.example.com"
]
},
{
"name": "img-src",
"sources": [
"'self'",
"data:"
]
}
],
"effective": {
"script-src-elem": "script-src",
"script-src-attr": "script-src",
"style-src-elem": null,
"style-src-attr": null,
"worker-src": "script-src",
"frame-src": null,
"connect-src": null,
"font-src": null,
"img-src": "img-src",
"manifest-src": null,
"media-src": null,
"object-src": null,
"base-uri": null,
"form-action": null,
"frame-ancestors": null
},
"warnings": [
"No default-src, so these are unrestricted: style-src-elem, style-src-attr, frame-src, connect-src, font-src, manifest-src, media-src, object-src.",
"object-src is not 'none': plugins (<object>, <embed>) can load. object-src 'none' is the usual hardening.",
"No base-uri: an injected <base> can redirect relative script URLs. base-uri 'none' or 'self' closes that."
]
}
],
"findings": [
{
"kind": "inline-script",
"line": 4,
"snippet": "<script nonce=\"r4nd0m\">start()",
"directive": "script-src-elem",
"url": null,
"hash": "'sha256-DIm7WJS6ZKDYe5qFLPy+h4JFI9Bol5QmYC57mt3Fb00='",
"fix": null,
"allowed": true,
"by": "'nonce-r4nd0m'",
"reason": null
},
{
"kind": "external-script",
"line": 5,
"snippet": "<script src=\"https://cdn.example.com/lib.js\">",
"directive": "script-src-elem",
"url": "https://cdn.example.com/lib.js",
"hash": null,
"fix": null,
"allowed": true,
"by": "https://cdn.example.com",
"reason": null
},
{
"kind": "external-script",
"line": 6,
"snippet": "<script src=\"https://evil.example.net/x.js\">",
"directive": "script-src-elem",
"url": "https://evil.example.net/x.js",
"hash": null,
"fix": "Add https://evil.example.net (or the exact URL) to script-src.",
"allowed": false,
"by": null,
"reason": "script-src has no source matching https://evil.example.net"
},
{
"kind": "image",
"line": 8,
"snippet": "<img src=\"/logo.png\">",
"directive": "img-src",
"url": "https://www.example.com/logo.png",
"hash": null,
"fix": null,
"allowed": true,
"by": "'self'",
"reason": null
},
{
"kind": "image",
"line": 8,
"snippet": "<img src=\"https://img.example.org/a.png\">",
"directive": "img-src",
"url": "https://img.example.org/a.png",
"hash": null,
"fix": "Add https://img.example.org (or the exact URL) to img-src.",
"allowed": false,
"by": null,
"reason": "img-src has no source matching https://img.example.org"
},
{
"kind": "event-handler",
"line": 8,
"snippet": "<button onclick=\"go()\">",
"directive": "script-src-attr",
"url": null,
"hash": "'sha256-5KYv+PUboo5h+0+YAtGRPbwv5d/QxzHslP4YGnUaxRw='",
"fix": "Move the event handler into an external script (addEventListener), or add 'unsafe-hashes' 'sha256-5KYv+PUboo5h+0+YAtGRPbwv5d/QxzHslP4YGnUaxRw=' to script-src.",
"allowed": false,
"by": null,
"reason": "script-src: no 'unsafe-inline', no matching hash"
}
],
"hashes_to_add": [
{
"directive": "script-src",
"sources": [
"'unsafe-hashes'",
"'sha256-5KYv+PUboo5h+0+YAtGRPbwv5d/QxzHslP4YGnUaxRw='"
],
"note": "Includes 'unsafe-hashes', which lets these hashes allow event handlers and style attributes. Adding a hash or nonce make… (56 more characters)"
}
],
"summary": {
"total": 6,
"allowed": 3,
"blocked": 3,
"ignored_data_blocks": 0
},
"warnings": []
}Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "csp_check",
"arguments": {
"html": "<!doctype html>\n<head>\n<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'nonce-r4nd0m' https://cdn.example.com; img-src 'self' data:\">\n<script nonce=\"r4nd0m\">start()</script>\n<script src=\"https://cdn.example.com/lib.js\"></script>\n<script src=\"https://evil.example.net/x.js\"></script>\n</head>\n<body><img src=\"/logo.png\"><img src=\"https://img.example.org/a.png\"><button onclick=\"go()\">Go</button></body>",
"url": "https://www.example.com/"
}
}
}Lint a policy on its own
curl 'https://util.caruana.me/api/csp/check?policy=default-src%20'\''self'\''%3B%20script-src%20'\''self'\''%20'\''unsafe-inline'\''%20'\''unsafe-eval'\''%20https%3A'Response 200
{
"origin": null,
"policies": [
{
"source": "header",
"line": null,
"text": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:",
"directives": [
{
"name": "default-src",
"sources": [
"'self'"
]
},
{
"name": "script-src",
"sources": [
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
"https:"
]
}
],
"effective": {
"script-src-elem": "script-src",
"script-src-attr": "script-src",
"style-src-elem": "default-src",
"style-src-attr": "default-src",
"worker-src": "script-src",
"frame-src": "default-src",
"connect-src": "default-src",
"font-src": "default-src",
"img-src": "default-src",
"manifest-src": "default-src",
"media-src": "default-src",
"object-src": "default-src",
"base-uri": null,
"form-action": null,
"frame-ancestors": null
},
"warnings": [
"script-src: 'unsafe-inline' allows every inline script and event handler, which defeats most XSS protection.",
"script-src: 'unsafe-eval' allows eval() and new Function().",
"script-src: https: lets scripts load from anywhere that scheme reaches.",
"object-src is not 'none': plugins (<object>, <embed>) can load. object-src 'none' is the usual hardening.",
"No base-uri: an injected <base> can redirect relative script URLs. base-uri 'none' or 'self' closes that."
]
}
],
"findings": [],
"hashes_to_add": [],
"summary": {
"total": 0,
"allowed": 0,
"blocked": 0,
"ignored_data_blocks": 0
},
"warnings": []
}Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "csp_check",
"arguments": {
"policy": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:"
}
}
}