Changeset 73abbfd in Klonkt for src/server.js


Ignore:
Timestamp:
06/28/2026 01:55:31 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
be5d86c
Parents:
9910ba1
Message:

experiment(csp): strict script-src (nonce + strict-dynamic) — BETA canary

Drops 'unsafe-inline' + broad host sources from script-src; a per-request nonce is injected into
every <script> at render time and 'strict-dynamic' covers htmx-swapped + player-API scripts.
Testing on BETA only first — htmx nav / embeds may break under the strict policy; do NOT roll to
the fleet until validated.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/server.js

    r9910ba1 r73abbfd  
    9191const server = http.createServer(app);
    9292
     93// Per-request CSP nonce for the strict script-src (nonce + strict-dynamic). Must be set
     94// before helmet builds the CSP header below. The nonce is injected into every <script> tag
     95// at render time (see middleware/render.js injectCspNonce).
     96app.use((req, res, next) => { res.locals.cspNonce = crypto.randomBytes(16).toString('base64'); next(); });
     97
    9398app.use(helmet({
    9499  contentSecurityPolicy: {
    95100    directives: {
    96101      defaultSrc: ["'self'"],
     102      // Strict CSP: a per-request nonce + 'strict-dynamic' (no 'unsafe-inline', no broad host
     103      // sources — securityheaders/Observatory flag those). Trusted (nonce'd) scripts may load
     104      // further scripts, which covers htmx-swapped inline scripts AND the external player APIs
     105      // that embed-player.js injects (YouTube/SoundCloud/Spotify). The nonce is added to every
     106      // <script> tag at render time (middleware/render.js injectCspNonce).
    97107      scriptSrc: [
    98         "'self'",
    99         "'unsafe-inline'",
    100         // Our custom embeds (embed-player.js) load the OFFICIAL player APIs
    101         // from these hosts. Without this whitelist the CSP silently blocks them
    102         // (only a console error) and the embed player fails.
    103         "https://www.youtube.com",   // YouTube IFrame Player API (+ www-widgetapi.js)
    104         "https://s.ytimg.com",       // YouTube player assets
    105         "https://w.soundcloud.com",  // SoundCloud Widget API (api.js)
    106         "https://open.spotify.com",  // Spotify iFrame API (loader)
    107         "https://*.spotifycdn.com",  // Spotify iFrame API (real bundle: embed-cdn.spotifycdn.com)
     108        "'strict-dynamic'",
     109        (req, res) => `'nonce-${res.locals.cspNonce}'`,
    108110      ],
    109111      // Helmet's default sets script-src-attr to 'none', which blocks ALL inline
Note: See TracChangeset for help on using the changeset viewer.