Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lab/events/limits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
36 changes: 36 additions & 0 deletions tests/lab/lab-post-merge-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,42 @@ 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 `+` — the
// polynomial-ReDoS shape static analysis flags — though the group cannot in
// practice fail mid-run, so no input separates the old verdicts from the new.
// These cases therefore pin the contract rather than a measurable slowdown:
// each stays under the 4 KiB field cap so it reaches the regex, and any rewrite
// that changes one of these verdicts is wrong regardless of speed. The elapsed
// assertion below measures the new shape on these inputs instead of relying on
// a timeout that the old shape would not have hit either.
const rejected = [
`cwd=/${"a/".repeat(2000)}`, // long segment chain
`cwd=/${"a//".repeat(1300)}`, // slash-dense chain
`cwd=/${"a".repeat(3000)}/`, // trailing slash after a maxed segment
"cwd=/a//b", // interior double slash keeps matching
"cwd=/a\tb", // a tab inside a segment is still a path
];
const startedAt = performance.now();
for (const detail of rejected) {
try {
enforceEventStructureLimits({ detail });
throw new Error(`expected raw_path rejection for ${detail.slice(0, 40)}`);
} catch (err) {
expect((err as { code?: string }).code).toBe("raw_path");
}
}

const allowed = [
`https://example.com/${"a/".repeat(2000)}`, // a URL, not a POSIX path
"cwd=//", // a leading double slash never starts a path
];
for (const detail of allowed) {
expect(() => enforceEventStructureLimits({ detail })).not.toThrow();
}
expect(performance.now() - startedAt).toBeLessThan(1_000);
}, 10_000);

test("invalid JSON contract artifacts classify as artifact_mismatch", () => {
const home = tempHome();
const artifactsDir = join(home, "artifacts");
Expand Down
Loading