A strict CSP is cheap. Keeping it strict is a grep.

The policy took an afternoon. The tripwire that fails the build on the first inline style is what it will still be enforcing next year.

Reference: Security

Every page on this site is served with a Content-Security-Policy, the header that tells a browser which origins a page may load code from. Ours carries no unsafe-inline, with one exception this post will get to. Writing that policy took an afternoon. I want to talk about the part that is not the afternoon: how it stays true while a hundred pages get edited around it. That is a maintenance problem, and maintenance problems on this repository get the same treatment as engine claims. Something greps.

The tripwire

CI runs chained greps over src/web/: an inline <script>, an inline <style> or a style= attribute anywhere in the served pages fails the build. Plain POSIX grep, no lookahead, because the check must run identically everywhere. The one exemption is application/ld+json, and the reasoning is worth keeping. A JSON-LD block is structured data for search engines, not code: the browser never executes it. So the strict CSP neither blocks it nor needs to cover it, and rich results get to keep quoting it.

The tripwire is what makes the policy a property of the repository rather than a property of the day it was written. An inline style is never added maliciously. It is added at 11pm, on a page nobody will review for punctuation, by someone fixing one margin. The grep does not care how reasonable the margin was.

The default that falsified a privacy claim

Here is the embarrassing find, kept because it is the useful one. This policy is assembled through helmet, a standard Node library for security headers. Helmet's default for fonts is font-src 'self' https: data:. That is every HTTPS origin on earth.

Our policy object simply never mentioned fonts, so that default is what production served. Meanwhile the privacy page said, in writing, that nothing on any page loads a script, font, image or stylesheet from another domain, and that the CSP forbids it. On fonts, the policy permitted exactly what the sentence swore it forbade.

Nothing on the site wants a remote font. The stack is system-ui. So font-src is pinned to 'self', without data:. No page inlines a font either, and "just in case" would reopen a narrower version of the same gap.

The lesson generalises. A policy you did not state is a policy someone else chose. That someone else was optimising for not breaking anybody's site, which is the opposite of a promise.

The one exception, and how it is prevented from spreading

/docs is Swagger UI, the interactive API-reference viewer. It renders markdown at runtime and needs style-src 'unsafe-inline'. It gets that one directive relaxed and nothing else. The route's policy is derived from the shared cspDirectives() function with two keys overridden, rather than hand-written.

That is not tidiness. The hand-written version is how /docs once shipped missing three directives every other route had. A copied string does not receive updates.

The derived version is also narrower than the site policy in one respect. The Turnstile origins the checker pages need, which is Cloudflare's bot check, are dropped: a docs page has no use for them. Inheriting privileges you do not use is its own small rot.

What the policy ends up designing

The fun of a strict CSP is that it becomes an architecture critic with commit access. A few of its verdicts around here:

  • The owner stats page has no charting library, because the CSP would block one. Four rectangles did not need a dependency anyway. Bar widths are set through the CSSOM, the JavaScript view of a page's styles, which CSP does not restrict. It is the same technique the quota bar has always used.
  • The blog covers, including the one above this article, are inline SVG coloured entirely with var(--token) and currentColor. A cover cannot carry a style= attribute, because the index generator refuses it, because the CI grep would refuse the index. The constraint that began as security is also what makes one drawing correct in both colour themes.
  • The Product Hunt badge is self-hosted as two local SVGs, with the live vote count trimmed off. Hotlinking it would widen img-src and quietly falsify the same privacy sentence the fonts almost did.
The claim is checkable, which is the point

None of this asks for trust. curl -D- against any page shows the header. The privacy and security pages state the policy in prose, and the MVSP self-assessment, which is the Minimum Viable Secure Product checklist buyers send, names the tripwires. A security property you can verify from outside is worth more than a longer list of properties you cannot. That is the same argument this product makes about document forensics, applied to itself.

Check it yourself

Run curl -D- https://tamperlens.com/ -o /dev/null and read the policy line. Then, if you like, try to find an inline style in the served HTML of any page. The build system has been trying every commit.

The security page documents the headers, the hardening and the receipt every production response signs. It is written for the reviewer who checks rather than the one who skims.