From 33cbfc5add35bb854e169b9ab1ffe41ce1e461f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 14:55:12 +0000 Subject: [PATCH] fix(security): render step info as text; add CSP and anti-framing headers Step info: StepsTimeline parsed a step's `info` as HTML after DOMPurify. That blocked scripts, but still let through forms, password inputs, inline styles (e.g. a full-screen overlay) and protocol-relative links, so anyone with an API token for a team could show teammates or admins a fake "session expired" form that posted off-site. Step info is now rendered as text, and only http(s) URLs become links, built as React elements with rel="noopener noreferrer". Headers: a new src/proxy.js sets, on every page: - a Content-Security-Policy with a per-request nonce (Next.js applies it to its own scripts; RuntimeConfigScript reads it from x-nonce). connect-src is the API origin from ANGLES_API_BASE_URL at runtime. form-action 'self' means no form can submit off-site. - frame-ancestors 'none' and X-Frame-Options: DENY, so the UI cannot be framed for clickjacking. - X-Content-Type-Options: nosniff and Referrer-Policy. Styles keep 'unsafe-inline' because rsuite and ApexCharts set inline styles at runtime, which a nonce cannot cover. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KgQXSUjuLVXmLWxobnMfSf --- src/app/runtime-config-script.js | 8 +- .../execution-timeline/StepsTimeline.js | 23 +++--- src/proxy.js | 80 +++++++++++++++++++ 3 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 src/proxy.js diff --git a/src/app/runtime-config-script.js b/src/app/runtime-config-script.js index 0391f19..2a042fd 100644 --- a/src/app/runtime-config-script.js +++ b/src/app/runtime-config-script.js @@ -1,12 +1,18 @@ +import { headers } from 'next/headers'; import { getAnglesApiUrl, RUNTIME_CONFIG_GLOBAL } from '../utils/runtime-config'; // Server component. Rendered before any client code runs so that // `window.__ANGLES_CONFIG__` is populated by the time modules that read it // (e.g. the axios base URL in Shell.js) execute. -export default function RuntimeConfigScript() { +// +// Carries the request's CSP nonce (set by src/proxy.js); without it the policy blocks +// this inline script like any other. +export default async function RuntimeConfigScript() { const config = { anglesApiUrl: getAnglesApiUrl() }; + const nonce = (await headers()).get('x-nonce') || undefined; return (