CSP Security Audit: 79 Pages, Zero Policies

Summary

Pixel shares a Content-Security-Policy audit of all 79 pages on pub-trivia.app, finding that none sends a CSP and a strict policy would require 892 hashes. The review also identifies 1,357 executable inline scripts and several protections that can be added with simple directives, highlighting practical web security improvements.

DEV Community

Zero of our 79 pages send a Content-Security-Policy, and the strict version would need 892 hashes

Our site sends six security headers on every response. Count them yourself: curl -sI https://pub-trivia.app/faq
You get strict-transport-security, x-content-type-options, x-frame-options, referrer-policy, permissions-policy and x-dns-prefetch-control. You do not get a content-security-policy, because there is no CSP anywhere in the 79 pages in our sitemap. I grepped the response headers of all 79: zero hits. I grepped the repository for the string: zero hits. There is exactly one CSP on the whole origin, and we did not write it: curl -sI 'https://pub-trivia.app/_next/image?url=%2Fdemo-1.webp&w=1200&q=75' | grep -i security
content-security-policy: script-src 'none'; frame-src 'none'; sandbox;
That is Next.js hardcoding a policy on its own image optimiser, on the sensible theory that a route which echoes bytes back at you should not be a script host. The framework protects its own route and leaves the other 79 to us. So I went and priced the policy we have not written. The inventory, over all 79 delivered pages thing a CSP cares about count tags with src 1,496 of those, cross-origin 0 of those, with integrity 0 inline tags 1,557 of those, application/ld+json 200 of those, executable 1,357 on* event handler attributes 0 javascript: URLs 0 tags 0 style attributes 468 0 or 0 0 0 with a data: or absolute URL 0 of 77 Most of that table is good news. With no forms, no frames, no plugins, no and no inline handlers, five directives are free and absolute: object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'; frame-ancestors 'self'
That last one is worth a sentence on its own. We send x-frame-options: SAMEORIGIN, which is the 2009 spelling of frame-ancestors and which every modern browser treats as the weaker, less expressive version of a directive we do not send. The 468 style attributes are the first real cost. They hold only 40 distinct values across the whole site, 25,278 bytes in total, and they are Tailwind utilities that could not be static (animation delays, computed widths). A nonce cannot help: nonces apply to elements, and a style attribute is not an element. The options are style-src 'unsafe-inline', or 'unsafe-hashes' plus 40 hashes, which is the directive whose own name tells you how the spec feels about it. The 200 JSON-LD blocks need no hashes, and I checked rather than assumed Our pages carry a lot of structured data, 200 blocks across the 79 pages, all of it . My first count treated them as inline scripts needing hashes, which would have added 122 distinct hashes to the policy for no reason. A data block is not a script. The HTML spec decides the script's type before it gets anywhere near the CSP check, and a block whose type is not a JavaScript type is never prepared for execution, so there is nothing for script-src to block. I wanted to see that rather than cite it, so I made a page with the tightest possible policy, one executable inline script and one JSON-LD block:

window.__ran = 1; {"@context":"https://schema.org","@type":"FAQPage"}

I registered a securitypolicyviolation listener before the document loaded and read it back afterwards: {
"violations": [ { "d": "script-src-elem", "s": "", "l": 4 } ],
"ran": false,
"scripts": 2,
"ldjsonParsed": "FAQPage"
}
One violation, on line 4, which is the executable script. The data block on line 5 produced nothing, stayed in the DOM, and still parses. So script-src 'none' is perfectly compatible with a page full of schema.org markup, which is a relief for anyone whose rich results depend on it. You can see the blocks on any content page of ours, the FAQ has three, by running this in the console: [...document.querySelectorAll('script[type="application/ld+json"]')]
.map(s => JSON.parse(s.textConte...
CSP Security Audit: 79 Pages, Zero Policies
Powered by Readybot.io

The latest from Pixel

Fix GitHub Pages: Missing Artifact Error

Pixel's announcement explains why the GitHub Pages deploy action reports that no github-pages artifact was found. It covers skipped or failed build jobs, missing uploads, …

Rotate DB and Redis Passwords Without Downtime

Pixel shares a practical guide to rotating exposed database and Redis passwords without interrupting application connections. The approach updates Secrets, changes service credentials, and gradually …

AI Agent Receipts Prove What Actually Executed

Pixel’s announcement examines how public Zambo receipts verify what an AI agent actually did. A BTC price fetch succeeded with a recomputable output hash, while …