diff --git a/review-enrichment/src/analyzers/iac-misconfig.ts b/review-enrichment/src/analyzers/iac-misconfig.ts index 9734099bc6..4dbdbc1170 100644 --- a/review-enrichment/src/analyzers/iac-misconfig.ts +++ b/review-enrichment/src/analyzers/iac-misconfig.ts @@ -112,6 +112,13 @@ const DOCKER_SOCKET_MOUNT_RE = const HSTS_DISABLED_RE = /\bStrict-Transport-Security\b[^\n]*\bmax-age\s*=\s*0\b/i; const REFERRER_UNSAFE_URL_RE = /\bReferrer-Policy\b[^\n]*\bunsafe-url\b/i; const COOKIE_NOT_HTTPONLY_RE = /\bhttp[_-]?only\b[\s"'=:,-]*false\b/i; +const COOP_UNSAFE_NONE_RE = + /\bCross-Origin-Opener-Policy\b[^\n]*\bunsafe-none\b/i; +// The value must be exactly `0` (filter off), not `10`/`01` — `(?![0-9])` rejects a digit continuation. +// Accepts both `X-XSS-Protection: 0` and nginx `add_header X-XSS-Protection "0"`. +const XSS_PROTECTION_OFF_RE = + /\bX-XSS-Protection\b(?:\s*:\s*|\s+)["']?0(?![0-9])/i; +const FRAME_OPTIONS_ALLOWALL_RE = /\bX-Frame-Options\b[^\n]*\bALLOWALL\b/i; function* patchLines(patch: string): Generator { let start = 0; @@ -541,6 +548,24 @@ export function scanPatchForIacMisconfig( ) { return findings; } + if ( + COOP_UNSAFE_NONE_RE.test(body) && + pushFinding(findings, seen, path, newLine, "coop-unsafe-none", maxFindings) + ) { + return findings; + } + if ( + XSS_PROTECTION_OFF_RE.test(body) && + pushFinding(findings, seen, path, newLine, "x-xss-protection-off", maxFindings) + ) { + return findings; + } + if ( + FRAME_OPTIONS_ALLOWALL_RE.test(body) && + pushFinding(findings, seen, path, newLine, "frame-options-allowall", maxFindings) + ) { + return findings; + } newLine++; } diff --git a/review-enrichment/src/render.ts b/review-enrichment/src/render.ts index 4185cc7c17..95d587ced1 100644 --- a/review-enrichment/src/render.ts +++ b/review-enrichment/src/render.ts @@ -360,6 +360,12 @@ export function renderBrief( return "sets `Referrer-Policy: unsafe-url`, leaking the full URL (path and query) to cross-origin destinations"; case "cookie-not-httponly": return "sets `httpOnly: false` on a cookie, exposing it to JavaScript so an XSS can read it"; + case "coop-unsafe-none": + return "sets `Cross-Origin-Opener-Policy: unsafe-none`, allowing cross-origin pages to retain opener access"; + case "x-xss-protection-off": + return "disables the legacy XSS filter with `X-XSS-Protection: 0`"; + case "frame-options-allowall": + return "sets `X-Frame-Options: ALLOWALL`, permitting any site to embed this page in a frame (clickjacking risk)"; } }; diff --git a/review-enrichment/src/types.ts b/review-enrichment/src/types.ts index de18137576..849859a4fa 100644 --- a/review-enrichment/src/types.ts +++ b/review-enrichment/src/types.ts @@ -270,7 +270,10 @@ export interface IacMisconfigFinding { | "docker-socket-mount" | "hsts-disabled" | "referrer-policy-leak" - | "cookie-not-httponly"; + | "cookie-not-httponly" + | "coop-unsafe-none" + | "x-xss-protection-off" + | "frame-options-allowall"; } /** A newly-added dependency whose install compiles native code (npm node-gyp addon) or has no prebuilt wheel diff --git a/review-enrichment/test/iac-misconfig.test.ts b/review-enrichment/test/iac-misconfig.test.ts index eabb8d64e3..e8537491da 100644 --- a/review-enrichment/test/iac-misconfig.test.ts +++ b/review-enrichment/test/iac-misconfig.test.ts @@ -461,6 +461,9 @@ test("scanPatchForIacMisconfig flags insecure HTTP security-header settings", () ["+ add_header Strict-Transport-Security \"max-age=0\";", "hsts-disabled"], ["+ add_header Referrer-Policy \"unsafe-url\";", "referrer-policy-leak"], ["+ httpOnly: false", "cookie-not-httponly"], + ["+ add_header Cross-Origin-Opener-Policy \"unsafe-none\";", "coop-unsafe-none"], + ["+ add_header X-XSS-Protection \"0\";", "x-xss-protection-off"], + ["+ add_header X-Frame-Options \"ALLOWALL\";", "frame-options-allowall"], ]; for (const [added, kind] of cases) { const findings = scanPatchForIacMisconfig( @@ -482,6 +485,11 @@ test("scanPatchForIacMisconfig does not flag secure HTTP header values (incl. Ca "+ add_header Cache-Control \"max-age=0\";", "+ add_header Referrer-Policy \"strict-origin-when-cross-origin\";", "+ httpOnly: true", + "+ add_header Cross-Origin-Opener-Policy \"same-origin\";", + "+ add_header X-XSS-Protection \"1; mode=block\";", + "+ add_header X-Frame-Options \"SAMEORIGIN\";", + // A bare `unsafe-none` config key without the COOP header token must NOT fire the COOP rule. + "+ unsafe-none = false", ]; for (const added of safe) { assert.deepEqual(