Changeset 73abbfd in Klonkt for src/middleware


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/middleware/render.js

    r9910ba1 r73abbfd  
    2121import ActivityPubService from '../services/ActivityPubService.js';
    2222import { audioEnabled as audioFeatureEnabled } from '../config/features.js';
     23
     24// Add the per-request CSP nonce to every <script> tag that doesn't already have one, so the
     25// strict script-src (nonce + 'strict-dynamic') allows them — including scripts in htmx
     26// partials. HTML-escaped "&lt;script" in rendered content (e.g. sanitized post bodies) won't
     27// match, so this only touches real tags.
     28function injectCspNonce(html, nonce) {
     29  if (!html || !nonce) return html;
     30  return String(html).replace(/<script(?![^>]*\snonce=)/gi, () => `<script nonce="${nonce}"`);
     31}
    2332import { PLATFORMS as PLATFORMS_CATALOG } from '../services/PlatformIcons.js';
    2433import { t as i18nT, resolveLang, SUPPORTED as LANGS, LANG_NAMES } from '../services/i18n.js';
     
    204213        );
    205214      } catch (e) { /* skip chrome OOB */ }
    206       return res.send(pageContent + oobChrome);
     215      return res.send(injectCspNonce(pageContent + oobChrome, res.locals.cspNonce));
    207216    }
    208217
    209     // Full: wrap content in shell
     218    // Full: wrap content in shell (rendered to a string so we can inject the CSP nonce).
    210219    locals.pageContent = pageContent;
    211     res.render('shell', locals);
     220    const shellHtml = await ejs.renderFile(path.join(VIEWS_DIR, 'shell.ejs'), locals, { async: false });
     221    res.send(injectCspNonce(shellHtml, res.locals.cspNonce));
    212222  } catch (err) {
    213223    console.error('[renderPage] Error rendering', viewName, err);
Note: See TracChangeset for help on using the changeset viewer.