I work on OnePassApps, the team behind app.onepassapps.com, so this is our own site and our own build. Every number below was measured on 8 November 2026 with plain HTTPS - one GET per URL, no browser, no session, no cookie, no JavaScript.
We have written about our headers twice before. What the Server Sends Back lists the security set we send, and What app.onepassapps.com Sends Before JavaScript Runs shows the readable shell the server renders. Neither post asked the question this one answers: the script-src directive carries exactly one SHA-256 hash, and a hash is a promise about a specific file. Which file?
We can answer it by hashing the page ourselves, and the answer is stranger than expected.
The hash names one inline script, and it is the same file on every page
Our Content-Security-Policy is 1,199 bytes long and is byte-for-byte identical on all 56 URLs in our sitemap - one distinct value across 56 pages, not 56 variants. It contains exactly 1 sha256- hash:
sha256-Uk96zwJ2Cg3zp4L5PIIPtLI/vzkWqOvaXew7SqyYAsE=
That is the only inline script the policy allows, and script-src-attr 'none' means no inline event handlers either.
So: fetch each page, pull out every inline <script> block that has no src, hash its bytes with SHA-256, and compare the base64 digest to the one in the policy. On 33 of the 56 URLs, exactly one block matches - 1,316 bytes long, the same digest on every one of those pages (1 distinct digest across 33 pages).
The matched script is the theme lock. It reads localStorage, and if the stored value is light or system it overwrites it with dark; it also removes the legacy toolpass-accent-color key and pins toolpass-theme-preset to graphite. Its own comment says why:
// Colour themes are temporarily locked to Graphite.
The other 23 pages ship no inline script at all
Here is the part that is worth the read. The 23 URLs that do not contain the hashed script are exactly the 23 guide pages under /guides - every one of them, no exceptions:
| Route group | URLs | Inline executable script |
|---|---|---|
| Apps, studios, home, voting, plans, terms | 33 | the 1,316-byte theme script, matching the policy hash |
/guides and every page under it |
23 | none |
On all 23 guide pages the count of inline executable scripts is 0. The page still loads the app bundle (23 of 23) and the fallback script (23 of 23), so the guide pages are not static documents - they are the same React shell with the theme script left out.
And the theme still lands. All 56 pages serve <html> with class="dark" and data-theme="dark" already in the tag, so a guide page starts dark without running anything. The inline script on the other pages exists to catch the case where localStorage disagrees with what the server rendered - it is the one thing the server cannot do on its own.
The practical consequence: the policy is identical on all 56 pages, but only 33 of them use the one script it allows. A hash that is present on 23 pages where nothing matches it is harmless - the rule simply never fires - but it means the policy is written per-site, not per-route, while the pages it applies to are not uniform. You can see the same split on the main app: the theme script and the policy hash travel together on the app routes, and the guide routes carry the policy without the script.
What the other responses carry
The same policy travels with the error and redirect answers:
- The 404 miss page answers with the identical 1,199-byte policy, plus
Cache-Control: no-store. - The trailing-slash 308 answers with the identical policy too, and with no
Content-Typeand noCache-Controlat all.
The API host is the one place the rule changes. api.onepassapps.com sends no Content-Security-Policy and no Permissions-Policy - but it keeps the rest of the set: strict-transport-security: max-age=15552000; includeSubDomains, x-frame-options: SAMEORIGIN, x-content-type-options: nosniff, referrer-policy: no-referrer, cross-origin-opener-policy: same-origin, cross-origin-resource-policy: same-origin, x-permitted-cross-domain-policies: none, Cache-Control: private, no-store, max-age=0 and Content-Type: application/json. We covered that host's origin rules in the origin allow-list post; the missing CSP is the piece that had not been measured.
The security set, once more
For the record, the headers the document host sends on every one of the 56 URLs are: strict-transport-security: max-age=15552000; includeSubDomains, referrer-policy: no-referrer, x-frame-options: SAMEORIGIN, x-content-type-options: nosniff, x-permitted-cross-domain-policies: none, cross-origin-opener-policy: same-origin, cross-origin-resource-policy: same-origin, and a Permissions-Policy of 150 bytes with 10 directives - camera=(), microphone=(self), payment=(), geolocation=() and the rest. Each one is a single distinct value across all 56 pages.
Limits
One build, one day, plain HTTPS, 56 URLs from our own sitemap, plus the miss page, the redirect and one API request. The hash comparison is arithmetic over the bytes the server sent: no browser, no JavaScript, no session, no cookies. The counts of inline scripts come from parsing the served HTML, and the "which pages match the policy" answer is a digest comparison, not an inference. I did not run the theme script, so I am not claiming what a browser does with localStorage at runtime - only what the served bytes say and which pages carry them.
Top comments (0)