Skip to content

feat(landing): precompile JSX so CSP can drop 'unsafe-eval'#47

Merged
MorganOnCode merged 1 commit into
masterfrom
chore/landing-precompile-csp-tighten
May 15, 2026
Merged

feat(landing): precompile JSX so CSP can drop 'unsafe-eval'#47
MorganOnCode merged 1 commit into
masterfrom
chore/landing-precompile-csp-tighten

Conversation

@MorganOnCode
Copy link
Copy Markdown
Owner

Closes audit #13. Replaces Babel-standalone in the browser with a precompiled bundle so the production CSP can be tightened from `script-src: 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com https://unpkg.com\` to `script-src: 'self' https://static.cloudflareinsights.com\`.

That kills the eval class of attack surface (Babel was compiling JSX in the browser via `new Function()`) and drops the unpkg.com supply-chain dependency since React + ReactDOM are now bundled into our own artifact.

Approach (and why this is small, not a 1-week rewrite)

Zero source-file changes to landing/*.jsx. The 8 files still use the familiar `React.useState` / `ReactDOM.createRoot` global API; nothing was refactored to ESM imports. Instead a small `scripts/build-landing.mjs` concatenates the files in their existing load order, prepends a shim that imports React + ReactDOM into module scope, and feeds the result through esbuild.

  • Minimal diff (the 8 .jsx files are untouched in this PR)
  • Dev workflow stays understandable (each component is still a self-contained .jsx)
  • Production artifact is one clean bundle: `landing/dist/app.js`, ~185 KiB minified, ~438 KiB sourcemap

Files

File Change
`scripts/build-landing.mjs` New, ~80 lines, esbuild bundle with classic JSX transform
`package.json` + react@18.3.1, react-dom@18.3.1 (runtime); + @types/react, @types/react-dom, esbuild (dev); + `build:landing` script chained into `build`
`Dockerfile` Build stage now copies `landing/` + the build script and runs `pnpm build` (backend + landing); production stage pulls `landing/` (incl. `dist/`) from the build stage
`landing/index.html` Drops 3 unpkg.com `<script>` tags + 8 Babel `<script>` tags; adds `<script src="/dist/app.js" defer>`
`src/server.ts` CSP `script-src`: removes `'unsafe-eval'`, `'unsafe-inline'`, `https://unpkg.com\`
`tests/integration/landing.test.ts` Replaces 2 stale tests (which checked individual .jsx routes) with 3 new ones: bundle is referenced, Babel/unpkg are absent, CSP header is strict

What's intentionally NOT in scope

  • Inline <style> block in landing. Removing it would let us drop `'unsafe-inline'` from `style-src` too, but it's a bigger CSS refactor (the file has heavy inline-style usage AND React inline styles still trigger style-src). Future PR.
  • Refactor .jsx files to ESM modules with named hook imports. Better tooling but not security-relevant. Future PR if anyone wants the DX improvement.

Local verification I ran before this PR

  • `pnpm build:landing` — 184.8 KiB bundle in 63ms
  • `pnpm typecheck` / `pnpm lint` / `pnpm test` — 34 files / 460 tests passing (was 459)
  • `node --check landing/dist/app.js` — syntax OK
  • Built a side-by-side docker image and ran it on port 3001 (production container untouched), confirmed:
    • Container healthy
    • `GET /dist/app.js` returns 200 with `application/javascript`, ~185 KB
    • CSP header is the tightened version (`script-src 'self' https://static.cloudflareinsights.com\` — no eval, no unpkg)
    • HTML now references just the one `<script src="/dist/app.js" defer>`

Production deploy

This needs your usual `bash deploy.sh` on the VPS. The deploy will:

  1. Pull master
  2. Rebuild the image (now also runs `pnpm build:landing` in the build stage, producing landing/dist/app.js inside the container)
  3. Restart the facilitator container

Visual regression risk: the React rendering pipeline is the same (classic JSX transform, same React version) but you should load `https://cardano402.com\` after deploy and:

  • Check that the page renders identically (Normal mode, Dev mode toggle, all sections present)
  • Open dev tools console and confirm zero errors
  • Confirm the new CSP shows up in response headers (DevTools → Network → / → Response Headers)

If anything looks wrong, the rollback recipe in `docs/operations.md` works as before.

🤖 Generated with Claude Code

Closes audit #13. Replaces Babel-standalone in the browser with a
precompiled bundle so the production CSP can be tightened from:

  script-src: 'self' 'unsafe-inline' 'unsafe-eval'
              https://static.cloudflareinsights.com https://unpkg.com

to:

  script-src: 'self' https://static.cloudflareinsights.com

That removes the eval class of attack surface entirely (Babel was
compiling JSX in the browser via `new Function()`, which required
'unsafe-eval'), and drops the unpkg.com supply-chain dependency
since React + ReactDOM are now bundled.

## Approach

Zero source-file changes. The 8 .jsx files in landing/ still use the
familiar `React.useState` / `ReactDOM.createRoot` global API; nothing
was refactored to ESM imports. Instead a tiny `scripts/build-landing.mjs`
concatenates the files in their existing load order, prepends a shim
that imports React + ReactDOM, and feeds the result through esbuild.

This minimises the diff (the .jsx code is untouched), keeps the dev
workflow understandable (each component is still a self-contained .jsx
file), and gives us a clean production artifact (landing/dist/app.js,
~185 KiB minified, ~438 KiB sourcemap).

## Files

- `scripts/build-landing.mjs` -- new, ~80 lines, esbuild bundle of the
  concatenated .jsx with classic JSX transform
- `package.json` -- adds react@18.3.1 + react-dom@18.3.1 as runtime
  deps, @types/react + @types/react-dom + esbuild as dev deps, a
  `build:landing` script, and chains it into `build`
- `Dockerfile` -- build stage now copies `landing/` and the build
  script, runs `pnpm build` (both backend tsup AND landing esbuild),
  production stage pulls landing/ (including dist/) from the build
  stage instead of the host
- `landing/index.html` -- drops 3 unpkg.com `<script>` tags + 8
  `<script type="text/babel">` tags; adds a single
  `<script src="/dist/app.js" defer>`
- `src/server.ts` -- removes 'unsafe-eval', 'unsafe-inline', and
  unpkg.com from CSP script-src. style-src keeps 'unsafe-inline' for
  React's inline `style={{...}}` pattern and the inline `<style>`
  block (could be tightened in a future PR by extracting CSS, but
  the React-inline pattern is pervasive and not security-critical)
- `tests/integration/landing.test.ts` -- replaces two stale tests
  (which checked individual .jsx routes) with three new ones that
  verify the new bundle is referenced, Babel/unpkg are absent, and
  the CSP header is strict

`.gitignore` already excludes `landing/dist/` via the global `dist/`
rule. landing/dist/ is generated only by `pnpm build:landing`; the
production image gets it from the build stage where it's materialised.

## Local verification

- `pnpm build:landing` -- 184.8 KiB bundle in 63ms
- `pnpm typecheck` / `pnpm lint` / `pnpm test` -- 34 files / 460 tests
- `node --check landing/dist/app.js` -- syntax OK
- Docker image built side-by-side with prod, started clean, /health
  healthy, /dist/app.js served with `application/javascript`, CSP
  header confirmed strict (no unsafe-eval, no unpkg)

Production container was NOT touched during validation.

## What's NOT in this PR

- Inline `<style>` block in landing/index.html is unchanged. Removing
  it would let us drop 'unsafe-inline' from style-src too, but it's
  a bigger CSS refactor (the file has heavy inline-style usage and
  React inline styles still trigger style-src). Out of scope here.
- The 8 .jsx files keep using `React.useState` etc. globally. A
  future cleanup could convert them to ESM modules with named hook
  imports for better tooling support (autocomplete, jump-to-def).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MorganOnCode MorganOnCode merged commit c218805 into master May 15, 2026
5 checks passed
@MorganOnCode MorganOnCode deleted the chore/landing-precompile-csp-tighten branch May 15, 2026 12:12
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