mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
fix(producer): drop the ReDoS-prone literal-argv regex for a linear scan
CodeQL flagged js/redos on the all-literal argv matcher. It was right: the `(?:"[^"]*"\s*,?\s*)+` form nests a quantifier inside a quantifier with an optional separator, so whitespace can be matched two ways and a long non-matching argv backtracks exponentially. Replaced with a linear scan — find the spawn head, slice to the closing bracket, and check the entries — plus small named helpers. Same behaviour: an all-literal argv is treated as taking no input, an argv with a bare identifier still has to be understood (verified by adding one and watching the guard fail). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c81f68b592
commit
6c5403f7cd
@@ -77,20 +77,54 @@ function mentionsProbe(src: string): boolean {
|
|||||||
// Comments and doc prose describing a spawn are not a spawn:
|
// Comments and doc prose describing a spawn are not a spawn:
|
||||||
// `tts.test.mjs` explains `ffprobeDuration's spawnSync("ffprobe", ...) call`
|
// `tts.test.mjs` explains `ffprobeDuration's spawnSync("ffprobe", ...) call`
|
||||||
// in a comment and was reported as an unclassified caller.
|
// in a comment and was reported as an unclassified caller.
|
||||||
// A probe spawned with an all-literal argv takes no runtime input, so
|
// A probe spawned with an all-literal argv takes no runtime input, so there
|
||||||
// there is nothing to terminate: `spawnSync("ffprobe", ["-version"])` is a
|
// is nothing to terminate: `spawnSync("ffprobe", ["-version"])` is a
|
||||||
// capability check, not a file probe. Dropping those before the test keeps
|
// capability check, not a file probe. Dropping those keeps them out of the
|
||||||
// them out of the unclassified list without weakening it — an argv carrying
|
// unclassified list without weakening it — an argv carrying a bare
|
||||||
// a bare identifier (a path) still has to be understood.
|
// identifier (a path) still has to be understood.
|
||||||
const NO_INPUT_PROBE =
|
//
|
||||||
/(?:spawn|spawnSync|execFile\w*|exec)\s*\(\s*["'`]ff(?:probe|mpeg)["'`]\s*,\s*\[\s*(?:"[^"]*"\s*,?\s*)+\]/g;
|
// Split into a linear scan plus a per-body check rather than one regex.
|
||||||
const code = src
|
// The obvious `(?:"[^"]*"\s*,?\s*)+` form nests a quantifier inside a
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
// quantifier with an optional separator, so whitespace can be matched two
|
||||||
.replace(/\/\/[^\n]*/g, "")
|
// ways and it backtracks exponentially on a long non-matching argv — CodeQL
|
||||||
.replace(NO_INPUT_PROBE, "");
|
// flagged it as js/redos, correctly.
|
||||||
return /(?:spawn|spawnSync|execFile\w*|exec)\s*\(\s*[^,)]*(?:ffprobe|ffProbe|probeBin|probePath)/i.test(
|
const code = stripComments(src);
|
||||||
code,
|
return CALLS_PROBE.test(withoutNoInputProbes(code));
|
||||||
);
|
}
|
||||||
|
|
||||||
|
const PROBE_SPAWN_HEAD =
|
||||||
|
/(?:spawn|spawnSync|execFile\w*|exec)\s*\(\s*["'`]ff(?:probe|mpeg)["'`]\s*,\s*\[/g;
|
||||||
|
const CALLS_PROBE =
|
||||||
|
/(?:spawn|spawnSync|execFile\w*|exec)\s*\(\s*[^,)]*(?:ffprobe|ffProbe|probeBin|probePath)/i;
|
||||||
|
|
||||||
|
function stripComments(src: string): string {
|
||||||
|
return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every entry a string literal → no runtime path in this argv. */
|
||||||
|
function isLiteralOnlyArgv(body: string): boolean {
|
||||||
|
const entries = body
|
||||||
|
.split(",")
|
||||||
|
.map((e) => e.trim())
|
||||||
|
.filter((e) => e !== "");
|
||||||
|
return entries.length > 0 && entries.every((e) => /^"[^"]*"$/.test(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blank out `spawn("ffprobe", [ ...all literals... ])` occurrences. */
|
||||||
|
function withoutNoInputProbes(code: string): string {
|
||||||
|
let out = "";
|
||||||
|
let cursor = 0;
|
||||||
|
PROBE_SPAWN_HEAD.lastIndex = 0;
|
||||||
|
for (let m = PROBE_SPAWN_HEAD.exec(code); m !== null; m = PROBE_SPAWN_HEAD.exec(code)) {
|
||||||
|
const bodyStart = m.index + m[0].length;
|
||||||
|
const bodyEnd = code.indexOf("]", bodyStart);
|
||||||
|
if (bodyEnd === -1) continue;
|
||||||
|
if (!isLiteralOnlyArgv(code.slice(bodyStart, bodyEnd))) continue;
|
||||||
|
out += code.slice(cursor, m.index);
|
||||||
|
cursor = bodyEnd + 1;
|
||||||
|
PROBE_SPAWN_HEAD.lastIndex = cursor;
|
||||||
|
}
|
||||||
|
return out + code.slice(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SKIP_DIRS = new Set(["node_modules", "dist"]);
|
const SKIP_DIRS = new Set(["node_modules", "dist"]);
|
||||||
|
|||||||
Reference in New Issue
Block a user