Skip to content

Nothing in production sends the Content-Security-Policy — only vite preview does #81

Description

@usekaneo

Split out of CON-44, where the code review raised it as the one major finding and it was deliberately not fixed. CON-44 defined the policy, proved it on a real production build, and documented the gap honestly; this ticket closes it.

State after CON-44

src/csp.ts holds the single definition of CONTENT_SECURITY_POLICY, guarded by src/csp.test.ts. vite.config.ts attaches it through preview.headers, so npm run build && npm run preview serves the production artefact under the real header — verified with curl -sI.

That is the entire delivery mechanism. vite preview is a local static server. The repository contains no app Dockerfile, no nginx or Caddy config, no _headers, netlify.toml or vercel.json. docs/09-security-privacy.md says so in as many words and warns the reader not to read the section as "the app has a CSP".

So the ticket that was titled "the app has no second line of defence behind the Markdown sanitiser" has, so far, given the shipped app no second line of defence. The policy is real, tested and pinned — it is simply never sent to a browser outside a developer's machine.

Why this was split rather than fixed

Every concrete delivery mechanism picks a hosting family. public/_headers is Netlify and Cloudflare Pages; it is an inert text file on nginx, Caddy, S3 + CloudFront or a container. Adding one would produce something that looks like delivery while doing nothing almost everywhere — worse than a gap that is written down. The project has not chosen a host yet (see CON-4), so the choice belongs to whoever does.

Generating the file from CONTENT_SECURITY_POLICY rather than retyping it solves duplication, not this. Keep that in mind when the host is known: one definition, generated artefacts.

Task

  1. Pick the host, or confirm the one CON-4 picks.
  2. Wire the header there, generated from src/csp.ts so the policy still has exactly one definition — a small postbuild step writing the host's own format is preferable to a second copy a human keeps in sync.
  3. Verify against the deployed origin, not the preview: the response header is present on the document, and the browser console is free of CSP violations while rendering a page, highlighting a code block, loading an image from a foreign host, connecting to the relay with NIP-42 AUTH, and uploading through Blossom. docs/09-security-privacy.md carries the by-hand recipe for the last two, including the VITE_RELAY_URL=wss://… note — the local ws:// relay is blocked by the policy on purpose, so a default checkout cannot exercise them.
  4. Rewrite the closing risk paragraph in docs/09-security-privacy.md once it is no longer true. Do not leave a doc claiming a gap that has been closed — that is the same failure as claiming protection that does not exist, pointing the other way.

Acceptance

  • The header is delivered by the production host and shown to be, with evidence (response headers from the deployed origin).
  • The policy has one definition; nothing is retyped.
  • The five paths above are exercised under the live policy, with the result recorded rather than inferred.
  • docs/09-security-privacy.md matches reality again.
  • npm run typecheck && npm run lint && npm test && npm run build pass.

Depends on

CON-4 (choosing the host). If that is still open when this is picked up, this ticket is blocked on it rather than a reason to decide it in passing.

Affected: deployment configuration (to be determined), possibly a postbuild script, docs/09-security-privacy.md

Source: CON-44 code review, finding 1 (major), deferred 2026-09-14.


Task: xyj6z5hn7e6jstu59f6dwkzu

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions