Demos: Add Content Security Policy (CSP) validation#32731
Demos: Add Content Security Policy (CSP) validation#32731EugeniyKiyashko wants to merge 24 commits intoDevExpress:26_1from
Conversation
595a390 to
8a995cb
Compare
8a995cb to
29b9cf6
Compare
There was a problem hiding this comment.
Pull request overview
Adds automated CSP (Content Security Policy) “report-only” validation for the demos, so CI can detect CSP violations across jQuery + framework demos and publish a consolidated report.
Changes:
- Introduces an Express-based CSP report-only server for demos, plus scripts to run CSP checks and summarize violations.
- Extends TestCafe visual tests to inject a CSP violation listener and write violations into a report file when enabled.
- Adds GitHub Actions jobs to run CSP checks per framework and generate a merged step summary + artifacts.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds a root-level script to start demos with the CSP server. |
| apps/demos/utils/visual-tests/inject/csp-listener.js | Captures securitypolicyviolation events in the browser during tests. |
| apps/demos/utils/server/csp-server.js | Implements CSP Report-Only middleware, report endpoints, and nonce injection for framework demos. |
| apps/demos/utils/server/csp-report-summary.js | Adds a CLI summarizer for CSP violation reports. |
| apps/demos/utils/server/csp-check.js | Adds a headless Chrome crawler to visit demo pages and query CSP violations from the server. |
| apps/demos/testing/common.test.ts | Optionally injects the CSP listener and writes JSONL violation output during TestCafe runs. |
| apps/demos/package.json | Adds scripts to run CSP server/check/report and a CSP-enabled TestCafe run. |
| apps/demos/.gitignore | Ignores generated csp-reports/ output. |
| .github/workflows/visual-tests-demos.yml | Adds CI jobs to run CSP checks per framework and publish a consolidated report. |
| function visitPage(url) { | ||
| return new Promise((resolve) => { | ||
| const child = execFile(CHROME_PATH, [ | ||
| '--headless=new', | ||
| '--no-sandbox', | ||
| '--disable-gpu', | ||
| '--disable-software-rasterizer', | ||
| '--disable-dev-shm-usage', | ||
| '--screenshot=/dev/null', | ||
| '--window-size=100,100', | ||
| url, | ||
| ], { timeout: 30000 }, () => resolve()); | ||
| child.on('error', () => resolve()); | ||
| }); |
There was a problem hiding this comment.
visitPage resolves even if Chrome fails to start or returns an error (both the execFile callback and the error event unconditionally call resolve()). This can lead to false-green CSP checks (no pages actually loaded → no reports collected). Consider rejecting on process errors / non-zero exit codes and failing the run (or at least tracking failed navigations and setting a non-zero exit code).
| - name: Start CSP Server | ||
| run: node apps/demos/utils/server/csp-server.js 8080 & | ||
|
|
||
| - name: Run CSP Check | ||
| working-directory: apps/demos | ||
| env: | ||
| CSP_FRAMEWORKS: ${{ matrix.FRAMEWORK }} | ||
| CHROME_PATH: google-chrome-stable | ||
| run: node utils/server/csp-check.js |
There was a problem hiding this comment.
The CSP server is started in the background and the next step immediately runs the checker. Without a readiness check, this can race on slower runners and cause flaky failures. Add a short wait/health-check loop (e.g., poll /csp-violations until it responds) before running csp-check.js.
| const cspReportDir = join(__dirname, '..', 'csp-reports'); | ||
| const cspReportFile = join(cspReportDir, 'csp-violations.jsonl'); | ||
|
|
||
| const writeCspReport = (testName: string, framework: string, violations: any[]) => { | ||
| if (!violations.length) return; | ||
| if (!existsSync(cspReportDir)) { | ||
| mkdirSync(cspReportDir, { recursive: true }); | ||
| } | ||
| for (const v of violations) { | ||
| const entry = { | ||
| test: testName, | ||
| framework, | ||
| ...v, | ||
| }; | ||
| appendFileSync(cspReportFile, `${JSON.stringify(entry)}\n`); | ||
| } |
There was a problem hiding this comment.
CSP reporting appends to a fixed csp-violations.jsonl file but never clears it, so rerunning tests can mix stale violations into a new run. Consider truncating/removing the file once at startup when CSP_REPORT=true (and batching writes per test to reduce synchronous I/O in large runs).
| res.locals.cspNonce = nonce; | ||
| } | ||
|
|
||
| res.setHeader('Content-Security-Policy', buildCspHeader(demoKey, nonce, framework)); |
There was a problem hiding this comment.
The server logs "CSP Report-Only mode enabled", but the middleware sets the enforcing Content-Security-Policy header. This is either misleading output or will unintentionally block resources during CSP runs. Use Content-Security-Policy-Report-Only if the intent is validation-only, or update the startup logs/comment to reflect enforced mode.
| res.setHeader('Content-Security-Policy', buildCspHeader(demoKey, nonce, framework)); | |
| res.setHeader('Content-Security-Policy-Report-Only', buildCspHeader(demoKey, nonce, framework)); |
| const child = execFile(CHROME_PATH, [ | ||
| '--headless=new', | ||
| '--no-sandbox', | ||
| '--disable-gpu', | ||
| '--disable-software-rasterizer', | ||
| '--disable-dev-shm-usage', | ||
| '--dump-dom', | ||
| '--virtual-time-budget=5000', | ||
| '--window-size=100,100', | ||
| url, | ||
| ], { timeout: 30000 }, () => resolve()); | ||
| child.on('error', (err) => { | ||
| reject(new Error(`Failed to launch Chrome at "${CHROME_PATH}": ${err.message}`)); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
visitPage resolves successfully even when Chrome exits with an error (non-zero exit code, navigation failure, timeout). Because the execFile callback ignores the error argument, CSP checks can silently pass while pages fail to load. Reject the promise when the callback receives an error so the job fails on real page-load issues.
| const isCspEnabled = () => process.env.CSP_REPORT === 'true'; | ||
|
|
||
| const cspReportDir = join(__dirname, '..', 'csp-reports'); | ||
| const cspReportFile = join(cspReportDir, 'csp-violations.jsonl'); | ||
|
|
||
| const writeCspReport = (testName: string, framework: string, violations: any[]) => { | ||
| if (!violations.length) return; | ||
| if (!existsSync(cspReportDir)) { | ||
| mkdirSync(cspReportDir, { recursive: true }); | ||
| } | ||
| for (const v of violations) { | ||
| const entry = { | ||
| test: testName, | ||
| framework, | ||
| ...v, | ||
| }; | ||
| appendFileSync(cspReportFile, `${JSON.stringify(entry)}\n`); | ||
| } |
There was a problem hiding this comment.
The CSP report file is only appended to and never cleared/truncated when CSP reporting is enabled. Re-running the TestCafe suite locally (or in any environment that reuses the workspace) will accumulate stale violations and make the summary inaccurate. Consider deleting/truncating csp-violations.jsonl once at the start of the run (when CSP_REPORT=true) before appending entries.
| const fileSystemPath = resolve(demosBaseDir, widget, name, approach, indexFileName); | ||
|
|
||
| if (!fileSystemPath.startsWith(demosBaseDir)) { | ||
| response.status(403).send('Forbidden'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
The path traversal guard uses fileSystemPath.startsWith(demosBaseDir), which can be bypassed by paths like .../DemosX/... when .. segments are used in params (prefix match). Prefer a path.relative(demosBaseDir, fileSystemPath) check that rejects absolute paths and any relative path starting with .. to robustly prevent escaping the demos directory.
| - name: Start CSP Server | ||
| run: node apps/demos/utils/server/csp-server.js 8080 & | ||
|
|
||
| - name: Run CSP Check | ||
| working-directory: apps/demos | ||
| env: | ||
| CSP_FRAMEWORKS: ${{ matrix.FRAMEWORK }} | ||
| CHROME_PATH: google-chrome-stable | ||
| run: node utils/server/csp-check.js | ||
|
|
There was a problem hiding this comment.
Same as in the jQuery job: the CSP server is started in the background and the check runs immediately. Add a readiness wait (poll an endpoint) to avoid intermittent connection errors in CI.
No description provided.