What happens
The admin shell hangs forever on "Checking your access…" and the browser console shows:
Executing inline script violates the following Content Security Policy directive
'script-src 'sha256-<A>''.
Either the 'unsafe-inline' keyword, a hash ('sha256-<B>'), or a nonce is required.
The blocked script is the one that resolves the access check, so the page fails
closed at its initial placeholder text.
Why
The HTML parser normalizes newlines to LF while preprocessing the input stream,
so a browser always hashes the LF form of an inline script. Both CSP hashes
are computed over the raw source instead:
plugins/admin/src/index.ts:31 — ADMIN_SCRIPT is sliced out of index.html,
which readFileSync returns verbatim, CRLF included.
plugins/auth/src/pages.ts:18 — CONFIRM_SCRIPT_HASH is computed over a
multi-line template literal, so it carries whatever newlines the checkout has.
On an LF checkout the two agree by accident. On a CRLF checkout they can't, and
CSP blocks the script.
Reproduced on Windows with git config core.autocrlf=true, which is the default
from the Git for Windows installer. Hashing the admin inline script at main
(ADMIN_BASE_PATH=/admin) both ways shows the two forms can never agree:
| script bytes |
sha256 (base64) |
| CRLF, as on disk |
8nnxfsr/p6r/aieabKVvy12q73z5l1I4PD+qnXcWfSk= |
| normalized to LF |
zu5GRQW+lyWKU5O6ueL0RTJ/IzwCrSZCGLta04leSy0= |
The first is what the header declares on a CRLF checkout; the second is what the
browser computes. The exact pair depends on the current index.html, but the
mismatch does not — it follows from the newline form alone.
Nothing rewrites the response in transit: a direct curl returns a CRLF body
alongside the CRLF-derived header, which is self-consistent. The mismatch is
introduced by the browser's own normalization, correctly.
plugins/auth/src/pages.ts has the same defect. It affects the sign-in confirm
page at GET /verify, so the sign-in flow breaks the same way.
Fix
Normalize CRLF to LF before hashing, in both places. That makes the hash
independent of how the file was checked out.
Worth noting the tests can't catch this — plugins/admin/test/whoami.test.ts:45
and plugins/auth/test/flow.test.ts:477 assert only that a sha256- hash is
present, not that it matches the served script.
What happens
The admin shell hangs forever on "Checking your access…" and the browser console shows:
The blocked script is the one that resolves the access check, so the page fails
closed at its initial placeholder text.
Why
The HTML parser normalizes newlines to LF while preprocessing the input stream,
so a browser always hashes the LF form of an inline script. Both CSP hashes
are computed over the raw source instead:
plugins/admin/src/index.ts:31—ADMIN_SCRIPTis sliced out ofindex.html,which
readFileSyncreturns verbatim, CRLF included.plugins/auth/src/pages.ts:18—CONFIRM_SCRIPT_HASHis computed over amulti-line template literal, so it carries whatever newlines the checkout has.
On an LF checkout the two agree by accident. On a CRLF checkout they can't, and
CSP blocks the script.
Reproduced on Windows with
git config core.autocrlf=true, which is the defaultfrom the Git for Windows installer. Hashing the admin inline script at
main(
ADMIN_BASE_PATH=/admin) both ways shows the two forms can never agree:8nnxfsr/p6r/aieabKVvy12q73z5l1I4PD+qnXcWfSk=zu5GRQW+lyWKU5O6ueL0RTJ/IzwCrSZCGLta04leSy0=The first is what the header declares on a CRLF checkout; the second is what the
browser computes. The exact pair depends on the current
index.html, but themismatch does not — it follows from the newline form alone.
Nothing rewrites the response in transit: a direct
curlreturns a CRLF bodyalongside the CRLF-derived header, which is self-consistent. The mismatch is
introduced by the browser's own normalization, correctly.
plugins/auth/src/pages.tshas the same defect. It affects the sign-in confirmpage at
GET /verify, so the sign-in flow breaks the same way.Fix
Normalize CRLF to LF before hashing, in both places. That makes the hash
independent of how the file was checked out.
Worth noting the tests can't catch this —
plugins/admin/test/whoami.test.ts:45and
plugins/auth/test/flow.test.ts:477assert only that asha256-hash ispresent, not that it matches the served script.