Browser hardening: a strict CSP and the work to earn it
Part of my Krabber series, a Twitter clone in Go. The full source is on GitHub.
Intro
A Content-Security-Policy is the browser’s instruction sheet for what your page is allowed to load and run. A strict one is one of the best defenses you have against cross-site scripting, because even if some user content slips through unescaped, the browser refuses to run it. Krabber sends a tight CSP on every response. But a strict policy isn’t a header you just paste in, it’s a header you have to make your app deserve, and that second part is the interesting bit.
I. The headers
The app sets a full set of security headers on every response. The main event is the CSP:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';
img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'none'; form-action 'self';
frame-ancestors 'none'; base-uri 'none'; object-src 'none'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
The important line is script-src 'self'. No inline scripts, no eval. Scripts can only come from my own origin, as files. That single restriction shuts the door on the most common way injected content runs. The frame-ancestors 'none' and X-Frame-Options: DENY stop anyone embedding Krabber in an iframe to clickjack it, and Permissions-Policy turns off camera, mic, location and payment, which the site never uses.
(style-src still allows 'unsafe-inline', because the templates use inline style attributes. Moving those into a stylesheet is a cleanup I owe the project. Styles are a much smaller risk than scripts, so it’s a known, bounded compromise, not an oversight.)
II. The work to earn “no eval”
Here’s the part nobody tells you: a strict script-src will break a normal app, because normal apps are full of little inline handlers. Getting Krabber to zero inline scripts and no eval took real cleanup.
The big one was htmx. I’d been using hx-on handlers, which need eval to run. I rewrote those as plain data- attributes (like data-reset-on-success) handled by one small app.js file, and then configured htmx to refuse the dangerous stuff entirely:
<meta name="htmx-config"
content='{"allowEval":false,"allowScriptTags":false,"selfRequestsOnly":true}'>
So htmx itself won’t eval, won’t run script tags from responses, and won’t fire requests at other origins. On top of that I deleted a pile of dead inline onclick handlers that were calling functions which didn’t even exist, and I moved the fonts off Google Fonts and self-hosted them, so font-src 'self' could stay strict. When Turnstile is turned on, the policy also allows challenges.cloudflare.com for its script and frame, and nothing else.
The payoff: I loaded the whole site with the browser console open and got zero CSP violations. That zero is the proof the policy is real and not just aspirational.
III. The Go side
Two habits back the CSP up in the server. First, I let html/template do its automatic escaping and never wrap user content in template.HTML, because that type is an explicit “trust this, don’t escape it,” and user input should never get that pass. The one place rich text is rendered, it’s built by a single trusted tokenizer that escapes everything else. Second, every page that shows user-specific data sends Cache-Control: no-store, so one krab’s page can’t get cached and shown to another.
Conclusion
A strict CSP is worth the trouble, but the trouble is the point: the header is easy, and the work is making your app actually run without inline scripts or eval. For Krabber that meant rewriting htmx handlers into data attributes, deleting dead code, and self-hosting fonts, until the console showed zero violations. If you want to tighten your own policy, turn script-src to 'self', open the console, and start fixing what screams. It’s tedious and completely worth it. Thanks for reading, and may your scripts only ever be your own.