Skip to content

CSP hash mismatch blocks the admin shell and auth confirm page on CRLF checkouts #232

Description

@foolish-pup

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:31ADMIN_SCRIPT is sliced out of index.html,
    which readFileSync returns verbatim, CRLF included.
  • plugins/auth/src/pages.ts:18CONFIRM_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions