Skip to content

Run the CSP check that has never run - #8

Merged
MichalAFerber merged 1 commit into
mainfrom
kim/run-the-csp-check
Sep 3, 2026
Merged

Run the CSP check that has never run#8
MichalAFerber merged 1 commit into
mainfrom
kim/run-the-csp-check

Conversation

@MichalAFerber

Copy link
Copy Markdown
Owner

Refs: branched from markdownwizard origin/main 9995aaa, fetched immediately before. Worktree; shared clone untouched.

No collision with draft PR #7 (feat/ds9-required-pages), which touches 404.html, css/app.css, docs.html, index.html, privacy.html, sitemap-0.xml, support.html, terms.html, tests/e2e.mjs. This PR touches tests/csp-check.mjs and .github/workflows/deploy.yml — disjoint. Handed over rather than folded into #7 at that author's request.

Two defects, both confirmed

1. Nothing invoked it. git grep "csp-check" origin/main returns exactly one hit: prose in README.md:107. Zero references in any .yml or .json. The test job ran node e2e.mjs and nothing else.

2. It could not have run anyway. Line 8 was const ROOT = '/home/user/markdownwizard'; — a path that exists on no Mac and no GitHub runner. Now derived from import.meta.url, the pattern e2e.mjs beside it already used.

So the repo carried a written-down safety argument that had never been executed, and a README that told readers CSP was covered in CI.

Verified red before trusted green

This is the only reason to believe the pass. I ran it locally against the real _headers CSP:

case exit reported
clean repo 0 CSP violations: none, console errors: none, preview rendered: yes
inline <script> injected into index.html 1 script-src-elem blocked inline

The failure message names the offending directive and offers the sha256-… hash, so a future red is actionable rather than merely red. index.html was restored — git status shows exactly the two intended files.

What it exercises: script-src 'self' https://plausible.thompsonblack.us with no 'unsafe-inline', against a print path that builds an iframe srcdoc containing an inline <style>. style-src does permit 'unsafe-inline', so the style is legitimate and the check confirms it stays legitimate.

One behavioural change worth calling out before merge

The deploy job declares needs: test. Adding this step to test means a CSP violation now blocks deployment, where previously it would not have been noticed at all. That is the intent, but it is a new gate and it should be a deliberate choice rather than a side effect — so flagging it rather than burying it.

Cost is one extra node run; it sits after the e2e step where chromium and deps are already installed.

Placement note

I put the step in the same job rather than a parallel one, because Install test deps + browser is the expensive part and it is already paid for there. A separate job would double a playwright install --with-deps chromium for one script.

Draft per the gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P5n34zWrrhGeihpHsCJE3K

tests/csp-check.mjs existed to prove the strict CSP in _headers does not break
the app: script-src carries no 'unsafe-inline', and the print path builds an
iframe srcdoc with an inline <style> that this file exercises. Nothing invoked
it. The only reference to it anywhere in the repo was prose in README.md:107,
so a reader reasonably concluded CSP was covered in CI. It was not.

It also could not have run if something had called it. Line 8 was

  const ROOT = '/home/user/markdownwizard';

an absolute path that exists on no Mac and no GitHub runner. ROOT now derives
from import.meta.url, matching e2e.mjs beside it, which already did this.

Wired into the test job after the e2e suite, where the browser and deps are
already installed by the existing step, so this costs one extra node run and no
new setup.

Verified red before being trusted green, which is the only reason to believe
the green:

  clean repo                          exit 0 — "CSP violations: none"
  inline <script> in index.html       exit 1 — "script-src-elem blocked inline"

The violation message names the directive and offers the sha256 hash, so a
future failure is actionable rather than just red.

Note a real behavioural change: the deploy job declares `needs: test`, so a CSP
violation now blocks deployment rather than merely reporting. That is the point
of the check, but it is a gate that did not exist before this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5n34zWrrhGeihpHsCJE3K
@MichalAFerber
MichalAFerber marked this pull request as ready for review September 3, 2026 21:16
@MichalAFerber

Copy link
Copy Markdown
Owner Author

Ready for review — attested against 9e3b0ff.

Read in full; at 17 lines there is no summary to hide behind. Two changes, and the second is what makes the first mean anything:

  1. The Strict-CSP check step is added to the test job, so a file that existed and was never executed now runs. deploy has needs: test, so this becomes a real gate on the deploy path rather than a reporting step.
  2. ROOT is derived from import.meta.url instead of the hardcoded /home/user/markdownwizard — a path that existed only in the sandbox where the file was authored. The check could not have run anywhere else even if something had invoked it, so the first change without the second would have gated deploys on a script that throws.

The reason I am flipping this quickly is the control, and I want it named because I ask for it constantly and rarely get it: the PR verified the check RED before trusting it green — an inline <script> added to index.html produces script-src-elem blocked inline and exit 1. A CSP check that has never failed is indistinguishable from one that cannot fail, and this one has now demonstrably done both.

My gate concerns:

  • New gate on the deploy path — blast radius checked. The Install test deps + browser step precedes both e2e.mjs and this, so chromium is present; the CSP check adds no dependency e2e.mjs does not already carry. The marginal flake risk is the one we already accept in this job.
  • deploy=SKIPPED on this PR is correct, not a gap. The job is if: github.event_name == 'push' || workflow_dispatch, so it is skipped by construction on pull_request. Worth stating because in this estate a skipped job showing green is exactly how a missing credential hides.

This also closes a finding Carrie raised independently on markdownwizard#7 — that tests/csp-check.mjs is invoked by nothing, not CI and not a script. It is the same defect, and this is its fix.

Michal merges.

@MichalAFerber
MichalAFerber merged commit 0b5adc8 into main Sep 3, 2026
2 checks passed
@MichalAFerber
MichalAFerber deleted the kim/run-the-csp-check branch September 3, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant