From a61893f2ed9535ce070b51e2ffceee9fc64285ee Mon Sep 17 00:00:00 2001 From: Frederik Baldauf Date: Tue, 23 Jun 2026 16:16:08 +0200 Subject: [PATCH] Add CSP to dev server --- csp.middleware.js | 138 ++++++++++++++++++++++++++++++++++++++++++++++ gulpfile.js | 15 ++++- 2 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 csp.middleware.js diff --git a/csp.middleware.js b/csp.middleware.js new file mode 100644 index 0000000..a862e00 --- /dev/null +++ b/csp.middleware.js @@ -0,0 +1,138 @@ +// Sets a Content-Security-Policy header in the DevServer analogous to the deployment. +// Override per developer via gulpfile.overrides.js (csp: false) or env CSP_POLICY. + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); + +// Fixed nonce per DevServer start (the header stays static, no per-request +// coordination needed). Sufficient for locally catching CSP violations. +const NONCE = crypto.randomBytes(16).toString("base64"); + +// CSP as a directive map, so origins/nonce can be added selectively. +const DIRECTIVES = { + "default-src": ["'self'"], + "object-src": ["'none'"], + "script-src": ["'self'", `'nonce-${NONCE}'`, "'unsafe-eval'", "'wasm-unsafe-eval'"], + "worker-src": ["'self'", "blob:"], + "child-src": ["'self'"], + "connect-src": ["'self'", "https:", "http:", "wss:", "ws:"], + "frame-src": ["'self'", "https:", "http:"], + "img-src": ["'self'", "data:", "blob:", "https://cdn.arcgis.com", "https:", "http:"], + "style-src": ["'self'", "'unsafe-inline'"], + "base-uri": ["'none'"], + "font-src": ["'self'", "data:"], + "form-action": ["'self'"], +}; + +// Directives the remote origin is added to when mapapps.remote.base is an +// absolute URL. +const REMOTE_DIRECTIVES = ["script-src", "style-src", "font-src", "img-src", "connect-src"]; + +// Default server dirs of the DevServer +const PROPERTY_DIRS = ["./target/webapp", "./target/test-classes"]; + +function readRemoteBaseFromProperties() { + let value; + for (const dir of PROPERTY_DIRS) { + const file = path.resolve(process.cwd(), dir, "application.properties"); + let content; + try { + content = fs.readFileSync(file, "utf-8"); + } catch (e) { + continue; + } + for (const line of content.split(/\r?\n/)) { + const m = line.match(/^\s*mapapps\.remote\.base\s*[=:]\s*(.+?)\s*$/); + if (m && !/^\s*[#!]/.test(line)) { + value = m[1]; + } + } + } + return value; +} + +function resolveRemoteOrigin() { + const raw = process.env.MAPAPPS_REMOTE_BASE ?? readRemoteBaseFromProperties() ?? "."; + if (!/^https?:\/\//i.test(raw)) { + return undefined; // "." or relative -> same-origin, covered by 'self' + } + try { + return new URL(raw).origin; // scheme://host[:port], path discarded + } catch (e) { + return undefined; + } +} + +function buildPolicy() { + const directives = {}; + for (const [name, sources] of Object.entries(DIRECTIVES)) { + directives[name] = sources.slice(); + } + const remoteOrigin = resolveRemoteOrigin(); + if (remoteOrigin) { + for (const name of REMOTE_DIRECTIVES) { + if (!directives[name].includes(remoteOrigin)) { + directives[name].push(remoteOrigin); + } + } + } + return `${Object.entries(directives) + .map(([name, sources]) => `${name} ${sources.join(" ")}`) + .join("; ")};`; +} + +// Determined once on load (remote.base does not change at runtime; restart the +// DevServer when it does). CSP_POLICY overrides completely. +const POLICY = process.env.CSP_POLICY || buildPolicy(); + +// Inserts the nonce attribute into all