From 3fa1f623a01ed977a68a1586a7b93028f871c5b4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:34:11 +0000 Subject: [PATCH] fix(lab): rewrite raw POSIX path regex without ambiguous alternation Code scanning flagged RAW_POSIX_PATH_RE as polynomial ReDoS (alert #1): the `(?:\/|[^/\0\r\n]+)+` alternation can repartition a run of path segments combinatorially on client-supplied event fields. Rewrite the segment run as an unambiguous `[^/]+(?:\/+[^/]+)*\/*` sequence so the match stays linear. The exec-side sibling regex was already rewritten on dev; this closes the remaining flagged pattern. Co-Authored-By: Epinephrine --- src/lab/events/limits.ts | 2 +- tests/lab/lab-post-merge-hardening.test.ts | 24 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/lab/events/limits.ts b/src/lab/events/limits.ts index 1447801a15e..e4a37c44633 100644 --- a/src/lab/events/limits.ts +++ b/src/lab/events/limits.ts @@ -32,7 +32,7 @@ const FORBIDDEN_EXACT_KEYS = new Set([ ]); const RAW_POSIX_PATH_RE = - /(?:^|[^A-Za-z0-9._~/])\/(?:(?=$|[^A-Za-z0-9._~/])|(?!\/)(?![ \t\r\n])(?:\/|[^/\0\r\n]+)+\/?(?=$|[^A-Za-z0-9._~/]))/u; + /(?:^|[^A-Za-z0-9._~/])\/(?:(?=$|[^A-Za-z0-9._~/])|(?!\/)(?![ \t\r\n])[^/\0\r\n]+(?:\/+[^/\0\r\n]+)*\/*(?=$|[^A-Za-z0-9._~/]))/u; const ASCII_URL_WHITESPACE_RE = /[\t\r\n]/g; const FILE_URI_RE = /(?:^|[^A-Za-z0-9+.-])file:/i; diff --git a/tests/lab/lab-post-merge-hardening.test.ts b/tests/lab/lab-post-merge-hardening.test.ts index 4642cdba8a3..05c1a65aa0c 100644 --- a/tests/lab/lab-post-merge-hardening.test.ts +++ b/tests/lab/lab-post-merge-hardening.test.ts @@ -224,6 +224,30 @@ test("event privacy admission rejects raw filesystem path bypass forms", () => { } }); +test("event privacy admission stays linear on pathological path strings", () => { + // RAW_POSIX_PATH_RE once alternated `\/` with `[^/]+` under a shared `+`; long segment runs + // were repartitioned combinatorially. Keep these just under the 4 KiB field cap so a + // backtracking regression surfaces as a timeout rather than a wrong verdict. + const deep = `cwd=/${"a/".repeat(2000)}`; + try { + enforceEventStructureLimits({ detail: deep }); + throw new Error("expected raw_path rejection for a long segment chain"); + } catch (err) { + expect((err as { code?: string }).code).toBe("raw_path"); + } + + const dense = `cwd=/${"a//".repeat(1300)}`; + try { + enforceEventStructureLimits({ detail: dense }); + throw new Error("expected raw_path rejection for a slash-dense chain"); + } catch (err) { + expect((err as { code?: string }).code).toBe("raw_path"); + } + + const notAPath = `https://example.com/${"a/".repeat(2000)}`; + expect(() => enforceEventStructureLimits({ detail: notAPath })).not.toThrow(); +}); + test("invalid JSON contract artifacts classify as artifact_mismatch", () => { const home = tempHome(); const artifactsDir = join(home, "artifacts");