https://github.com/didrod205/csp-doctor
Lint a Content-Security-Policy for XSS holes locally โ 'unsafe-inline', wildcards, missing directives, and allowlisted hosts that bypass CSP (JSONP/AngularJS). Nonce/strict-dynamic aware. Deterministic CLI, JSON/MD reports, no website.
https://github.com/didrod205/csp-doctor
appsec cli content-security-policy csp csp-evaluator devsecops linter security security-headers strict-dynamic typescript web-security xss zero-dependency
Last synced: 2 days ago
JSON representation
Lint a Content-Security-Policy for XSS holes locally โ 'unsafe-inline', wildcards, missing directives, and allowlisted hosts that bypass CSP (JSONP/AngularJS). Nonce/strict-dynamic aware. Deterministic CLI, JSON/MD reports, no website.
- Host: GitHub
- URL: https://github.com/didrod205/csp-doctor
- Owner: didrod205
- License: mit
- Created: 2026-06-05T01:03:05.000Z (13 days ago)
- Default Branch: main
- Last Pushed: 2026-06-05T01:32:18.000Z (13 days ago)
- Last Synced: 2026-06-05T03:11:00.251Z (13 days ago)
- Topics: appsec, cli, content-security-policy, csp, csp-evaluator, devsecops, linter, security, security-headers, strict-dynamic, typescript, web-security, xss, zero-dependency
- Language: TypeScript
- Size: 51.8 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# ๐ก๏ธ csp-doctor
### Lint your Content-Security-Policy for XSS holes โ locally, no website to paste into.
[](https://www.npmjs.com/package/csp-doctor)
[](https://bundlephobia.com/package/csp-doctor)
[](https://github.com/didrod205/csp-doctor/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/csp-doctor)
[](./LICENSE)
**[๐ Try the browser playground โ](https://didrod205.github.io/csp-doctor/)** ย ยทย paste a CSP, see its XSS holes ranked. Nothing is uploaded โ it all runs client-side.
You added a `Content-Security-Policy` to stop XSS. But a single `'unsafe-inline'`
in `script-src` silently turns the whole thing off, a wildcard or bare `https:`
lets any host serve scripts, and allowlisting a CDN like `ajax.googleapis.com`
opens a JSONP/AngularJS bypass โ so an attacker runs scripts *despite* your policy.
The only good analyzer, Google's CSP Evaluator, is **a website you paste into** โ
not something you can run in CI.
**csp-doctor lints a CSP for these holes locally and deterministically** โ from a
string, an HTML ``, or a headers file โ and it's **nonce / hash /
strict-dynamic aware**, so it won't cry wolf about an `'unsafe-inline'` that
modern browsers already ignore.
```bash
npx csp-doctor scan -p "default-src 'self'; script-src 'self' 'unsafe-inline' ajax.googleapis.com"
```
```
policy 57/100 (F)
โ 'unsafe-inline' in script-src defeats XSS protection [script-src]
โ Allowlisted host enables a CSP bypass: ajax.googleapis.com [script-src]
โ No base-uri [base-uri]
โ No frame-ancestors [frame-ancestors]
```
---
## Why csp-doctor?
- ๐ฏ **It knows the bypasses.** The built-in list of hosts that undermine an
allowlist (JSONP endpoints, hosted AngularJS) is the core insight behind Google's
CSP Evaluator โ baked in for offline use.
- ๐ง **Nonce / hash / strict-dynamic aware.** It understands that a nonce makes
`'unsafe-inline'` a harmless fallback, and that `'strict-dynamic'` ignores host
allowlists โ so it grades a *modern* CSP correctly instead of flagging everything.
- ๐ **Local & deterministic.** No website, no API key, runs offline and in CI.
Same policy โ same result. Fail the PR that ships `'unsafe-inline'`.
- ๐งฉ **Reads it from anywhere.** A raw policy, an HTML ``, an
`_headers` file, nginx `add_header`, Apache `Header set`, or `vercel.json`.
Why not paste it into an LLM? The bypassable-host list and directive-fallback rules
(`script-src` โ `default-src`, `base-uri` doesn't fall back) are exact, evolving
facts a chatbot gets wrong โ and you need this gating *every* CSP change, not once.
## Install
```bash
# run it now
npx csp-doctor scan -p ""
# or add it
npm install -g csp-doctor # global CLI
npm install -D csp-doctor # CI dependency
```
Node โฅ 18. The core is dependency-free and browser-safe (ready for a web playground).
## Quick start
```bash
csp-doctor scan -p "default-src 'self'; script-src 'self' 'nonce-abc'" # a string
csp-doctor scan index.html # from
csp-doctor scan _headers vercel.json # from configs
curl -sI https://example.com | csp-doctor scan # from live headers
csp-doctor scan -p "" --min-score 80 # CI gate
csp-doctor init # write a config
```
See [`examples/sample-report.md`](./examples/sample-report.md), and
[`examples/strong.csp.txt`](./examples/strong.csp.txt) for a policy that scores 100.
## What it checks
| Group | Examples |
| ----- | -------- |
| **XSS exposure** | `'unsafe-inline'` (error โ or info when a nonce/hash makes it moot), `'unsafe-eval'`, wildcard `*`, bare `https:`/`http:`, `data:` in `script-src` |
| **Allowlist bypass** | hosts known to break a CSP allowlist (JSONP / hosted AngularJS), unless `'strict-dynamic'` neutralizes the allowlist |
| **Missing directives** | no `object-src 'none'`, no `base-uri`, no `frame-ancestors`, no `default-src` fallback |
| **Hardening & meta** | `'strict-dynamic'` without a nonce, deprecated `report-uri`, and Report-Only policies (which **block nothing**) |
Each finding is a weighted error / warning / info; the policy rolls up to a 0โ100
score and an AโF grade you can gate in CI.
## Real scenarios
**1. Gate your CSP in CI.** A PR that adds `'unsafe-inline'` or a bypassable CDN to
your policy fails the build:
```yaml
# .github/workflows/ci.yml
- run: npx csp-doctor scan next.config.js --min-score 85 # or your _headers / meta
```
**2. Audit a policy before you ship it.** Paste the header you're about to deploy
and see the holes โ locally, without sending your config to a third-party site.
**3. Triage a security finding.** A scanner said "weak CSP" โ `csp-doctor scan` tells
you *which* directive and *why*, with the exact fix.
## Configuration
`csp-doctor init` writes `csp-doctor.config.json`:
```jsonc
{
"ignore": [], // rule ids to skip, e.g. ["missing-default-src"]
"bypassHosts": [], // extra hosts to treat as bypass-prone
"allowHosts": [], // hosts you've audited and accept (suppress the finding)
"minScore": 0 // CI gate threshold
}
```
## Library API
```ts
import { analyzeCsp, DEFAULT_CONFIG } from "csp-doctor";
const [report] = analyzeCsp("inline", "script-src 'self' 'unsafe-inline'", DEFAULT_CONFIG);
for (const f of report.findings) console.log(f.severity, f.rule, f.directive);
```
Also exported: `analyzePolicy`, `parsePolicies`, `extractPolicies`, `findBypass`,
`BYPASS_HOSTS`, and types. The core has zero runtime dependencies.
## Roadmap
- ๐ค **Optional `--ai` layer (bring-your-own key)** to draft a hardened
replacement policy for your app. The core stays 100% offline and deterministic.
- `require-trusted-types-for` / Trusted Types scoring.
- `style-src` and `connect-src` specific checks (CSS exfiltration, beacon hosts).
- Suggest the migration to a nonce + `'strict-dynamic'` policy automatically.
- โ
**A browser playground** โ paste a policy, see the audit, nothing uploaded.
[Live here](https://didrod205.github.io/csp-doctor/).
## ๐ Sponsor
csp-doctor is free and MIT-licensed, built and maintained in spare time. If it caught
a hole in your CSP, please consider supporting it:
- โญ **Star this repo** โ the simplest free way to help others find it.
- ๐ **[Sponsor via Lemon Squeezy](https://elab-studio.lemonsqueezy.com/checkout/buy/5d059b89-51d0-456b-b33a-ed56994f7010)** โ one-time or recurring.
> The bypassable-host insight is owed to the research behind
> [Google's CSP Evaluator](https://csp-evaluator.withgoogle.com). csp-doctor is an
> independent, offline implementation and is not affiliated with it.
## License
[MIT](./LICENSE) ยฉ csp-doctor contributors