fix(#2128): route <tag> scans through a hardened shared seam (root-cause ReDoS fix)
A convergence audit showed the `<tag>[\s\S]*?</tag>` lazy-scan ReDoS was pervasive (a dozen+ bespoke copies across roadmap-parser/check-command-router/verify), each a distinct quadratic vector on a large document with unclosed tags. Rather than whack-a-mole, single-source them (maintainer-directed): - markdown-sectionizer: extractTaggedBlocks now shares one ReDoS-safe `taggedBlockPattern` (stop-at-next-open, bounded optional attributes) and gains a `stripTaggedBlocks` companion for block removal. - roadmap-parser: 3 `<details>` strips -> stripTaggedBlocks (behavior-identical — no <details> here carries attributes). - verify: actionZones + both <task> loops + their nested <name>/<files>/gate/req extractions -> extractTaggedBlocks (behavior byte-equivalent, verified). - check-command-router: the objective|tasks?|action alternation hardened in place (distinct multi-tag shape); HTML-comment strip gains a `$` fallback. Every vector now linear (<3ms on 1.5MB adversarial); real content unchanged (end-to-end verify/roadmap resolution + task extraction confirmed). The only remaining `<!--…-->` scan (uat.cts:201) is anchored + non-global — one scan, safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -535,10 +535,7 @@ export function extractTaggedBlocks(content: string, tagName: string): string[]
|
||||
if (typeof content !== 'string' || content.length === 0) return [];
|
||||
if (typeof tagName !== 'string' || tagName.length === 0) return [];
|
||||
|
||||
// Escape the tag name for safe interpolation into a RegExp.
|
||||
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const pattern = new RegExp(`<${escapedTag}>((?:(?!<${escapedTag}>)[\\s\\S])*?)</${escapedTag}>`, 'g');
|
||||
|
||||
const pattern = taggedBlockPattern(tagName, 'g');
|
||||
const results: string[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
@@ -547,6 +544,36 @@ export function extractTaggedBlocks(content: string, tagName: string): string[]
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the single, ReDoS-safe `<tag>…</tag>` block regex shared by
|
||||
* `extractTaggedBlocks` (extract bodies) and `stripTaggedBlocks` (remove blocks).
|
||||
*
|
||||
* Safety: the body uses a `(?:(?!<tag[\s>])[\s\S])*?` negative-lookahead scan
|
||||
* that terminates at the NEXT opening `<tag>` instead of lazily rescanning the
|
||||
* whole remaining document for a `</tag>` that may never appear — so a large
|
||||
* document full of unclosed `<tag>` openings stays LINEAR, not quadratic
|
||||
* (#2128). The opening tag tolerates optional attributes (`<tag foo="bar">`),
|
||||
* bounded to 1000 chars so the attribute scan cannot itself ReDoS.
|
||||
* Group 1 is the block body.
|
||||
*/
|
||||
function taggedBlockPattern(tagName: string, flags: string): RegExp {
|
||||
const esc = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp(`<${esc}(?:\\s[^>]{0,1000})?>((?:(?!<${esc}[\\s>])[\\s\\S])*?)</${esc}>`, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove every `<tagName>…</tagName>` block (opening tag, body, and closing tag)
|
||||
* from `content`. The ReDoS-safe counterpart to `extractTaggedBlocks` — same
|
||||
* hardened pattern, `.replace(…, '')` instead of body extraction. Case-insensitive
|
||||
* by default (matching the `<details>` strip call sites); pass `caseSensitive`
|
||||
* to force exact-case matching.
|
||||
*/
|
||||
export function stripTaggedBlocks(content: string, tagName: string, caseSensitive = false): string {
|
||||
if (typeof content !== 'string' || content.length === 0) return '';
|
||||
if (typeof tagName !== 'string' || tagName.length === 0) return content;
|
||||
return content.replace(taggedBlockPattern(tagName, caseSensitive ? 'g' : 'gi'), '');
|
||||
}
|
||||
|
||||
// ─── replaceSection ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user