fix(#663): resolve open CodeQL/Dependabot security alerts (ReDoS, prototype pollution, workflow perms, qs DoS) (#665)

* fix(#663): resolve open CodeQL/Dependabot security alerts

- ReDoS: collapse ambiguous nested quantifiers in phase-heading regexes
  (verify/validate/commands) and the plan-filename lookahead (phase) to
  provably-equivalent non-backtracking forms
- prototype pollution: guard __proto__/constructor/prototype in setConfigValue
- remove dead no-op .replace(/-/g,'-') in phase.cts
- escape all regex metachars in bug-2839 test
- add contents:read permissions to security-scan + install-smoke workflows
- pin qs >= 6.15.2 via overrides (DoS GHSA)
- broaden prompt-injection allowlist to translated security-model docs

Closes #663

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#663): regression tests for prototype-pollution guard and roadmap-phase ReDoS

Behavioral test that config-set rejects __proto__/constructor/prototype keys
without polluting Object.prototype, plus a ReDoS guard (timing-bound) and
behavior-preservation assertions for the collapsed phase-heading regexes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#663): make ReDoS regression assert structured result, not elapsed time

Replace elapsed-time assertions (which tripped local/no-elapsed-assertion
ESLint rule and were unsound for synchronous ReDoS) with structured-result
assertions on adversarial inputs: assert that malformed phase headings/
unchecked-item lines without a terminating colon/space yield an empty Set,
which is both the correct behavior and an exercise of the fixed linear regex
on the catastrophic-backtracking input shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#663): add Security changeset fragment for #665

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#663): fold prototype-pollution regression into config.test.cjs

The standalone bug-663-config-prototype-pollution.test.cjs was a 9th
config-module test file, tripping lint-test-file-count (the allowlist is
ratcheted and must not grow). Consolidated into config.test.cjs instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-04 08:54:15 -04:00
committed by GitHub
parent 0afed31904
commit b238baddbf
14 changed files with 240 additions and 26 deletions

View File

@@ -643,7 +643,7 @@ function cmdValidateConsistency(cwd: string, raw: boolean): void {
const roadmapPhases = new Set<string>();
const phasePattern =
/#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:/gi;
/#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
let m: RegExpExecArray | null;
while ((m = phasePattern.exec(roadmapContent)) !== null) {
roadmapPhases.add(m[1]);
@@ -651,7 +651,7 @@ function cmdValidateConsistency(cwd: string, raw: boolean): void {
const fullRoadmapPhases = new Set<string>();
const fullPhasePattern =
/#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*(?:-[\w.-]+)*)\s*:/gi;
/#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
let fm: RegExpExecArray | null;
while ((fm = fullPhasePattern.exec(roadmapContentRaw)) !== null) {
fullRoadmapPhases.add(fm[1]);