Changeset 73abbfd in Klonkt
- Timestamp:
- 06/28/2026 01:55:31 PM (2 months ago)
- Branches:
- main
- Children:
- be5d86c
- Parents:
- 9910ba1
- Location:
- src
- Files:
-
- 2 edited
-
middleware/render.js (modified) (2 diffs)
-
server.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/middleware/render.js
r9910ba1 r73abbfd 21 21 import ActivityPubService from '../services/ActivityPubService.js'; 22 22 import { 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 "<script" in rendered content (e.g. sanitized post bodies) won't 27 // match, so this only touches real tags. 28 function injectCspNonce(html, nonce) { 29 if (!html || !nonce) return html; 30 return String(html).replace(/<script(?![^>]*\snonce=)/gi, () => `<script nonce="${nonce}"`); 31 } 23 32 import { PLATFORMS as PLATFORMS_CATALOG } from '../services/PlatformIcons.js'; 24 33 import { t as i18nT, resolveLang, SUPPORTED as LANGS, LANG_NAMES } from '../services/i18n.js'; … … 204 213 ); 205 214 } catch (e) { /* skip chrome OOB */ } 206 return res.send( pageContent + oobChrome);215 return res.send(injectCspNonce(pageContent + oobChrome, res.locals.cspNonce)); 207 216 } 208 217 209 // Full: wrap content in shell 218 // Full: wrap content in shell (rendered to a string so we can inject the CSP nonce). 210 219 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)); 212 222 } catch (err) { 213 223 console.error('[renderPage] Error rendering', viewName, err); -
src/server.js
r9910ba1 r73abbfd 91 91 const server = http.createServer(app); 92 92 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). 96 app.use((req, res, next) => { res.locals.cspNonce = crypto.randomBytes(16).toString('base64'); next(); }); 97 93 98 app.use(helmet({ 94 99 contentSecurityPolicy: { 95 100 directives: { 96 101 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). 97 107 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}'`, 108 110 ], 109 111 // Helmet's default sets script-src-attr to 'none', which blocks ALL inline
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)