Brewing Content Security Policies from real page loads
cspresso uses Playwright’s headless Chromium to crawl a site, observe what loads, and emit a draft Content-Security-Policy header. Over its first few releases it gained CSP bypass, policy evaluation, and non-HTML filtering.
The CSP problem
Content Security Policy is one of the most effective browser-side defences against XSS and data
exfiltration, but it’s notoriously fiddly to get right. You need to know every origin your app
loads from - scripts, styles, images, fonts, frames, WebSocket endpoints, API calls. Miss one
and the policy blocks legitimate traffic; add 'unsafe-inline' and you’ve defeated the point.
There are tools out there (and baked into the CSP standard itself) to run it in ‘report-only’ mode and see what violations occur. That’s still definitely a good idea, but it means you’ve needed to know where to start. It can also take a lot of time to analyse the results and determine what to do next to ‘fix’ a CSP that was never right to begin with.
The honest way to discover what your app loads is to let a real browser execute it and watch. That’s what cspresso does. It launches headless Chromium via Playwright , crawls same-origin links up to a page limit, and distils the observed origins into CSP directives. The output is a draft - a starting point to review and tighten before enforcing.
The beginning
The first release was simple: launch Chromium, crawl, print a CSP header. Playwright’s browser
binaries were auto-installed if missing. A --ignore-non-html option followed quickly, to skip
pages that weren’t HTML and could pollute the output.
What makes it useful
Two features have since turned cspresso from a crawler into an actually useful CSP rollout tool:
--bypass-cspstrips any existing CSP or CSP-Report-Only headers from the crawled HTML, so an already-enforcing policy doesn’t skew the results. This lets you re-discover the full set of origins your app could load, not just those your current policy permits.--evaluate CSPinjects a proposed CSP asContent-Security-Policy-Report-Onlyon crawled documents, reports any violations, and exits with code 1 if violations are detected. This turns cspresso into a CI-checkable tool: propose a policy, evaluate it against your live site, fail the pipeline if it would break anything.
The recommendation is to use them together: --bypass-csp --evaluate so the evaluation isn’t
influenced by the existing enforcement.
Keep in mind the security implications of --bypass-csp: if your site is compromised, bypassing
the CSP and executing the page in a browser - even headless - could evaluate malicious JavaScript
or CSS. This is a tool for analysing your own sites, not for pointing at untrusted URLs.
Correctness and polish
A few bugs have needed fixing over time to date. The --evaluate mode was initially injecting the
proposed CSP into third-party domains as well, causing them to throw violations and trip the
result - the fix ensured the proposed policy was only applied to same-origin documents. The wait
condition was also relaxed: instead of waiting for networkidle (which some pages never reach),
cspresso now proceeds if the load event fires, even if networkidle doesn’t arrive. This
fixed hangs on pages with persistent connections or long-polling.
Where it stands today
cspresso is a small, focused tool. It doesn’t try to be a full CSP linter or a security scanner.
It does one thing well: lets a real browser show you what your app loads, then turns that into a
policy you can review. The --evaluate mode makes it CI-friendly for staged CSP rollouts.
I have to admit I don’t spend a lot of time developing cspresso compared to some of my other tools. It probably deserves some more love!
If nerding out over CSPs is your thing, I also recommend having a look at Cory Myers’ webcat-csp-soundness project, and more generally at WEBCAT itself.
The source and full changelog are at git.mig5.net/mig5/cspresso , with documentation at cspresso.cafe .
- Language: Python
- Browser: headless Chromium (Playwright)
- Distribution: pip/pipx, AppImage