diff --git a/src/lab/events/limits.ts b/src/lab/events/limits.ts index f6b5ab1c91..1447801a15 100644 --- a/src/lab/events/limits.ts +++ b/src/lab/events/limits.ts @@ -33,6 +33,8 @@ 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; +const ASCII_URL_WHITESPACE_RE = /[\t\r\n]/g; +const FILE_URI_RE = /(?:^|[^A-Za-z0-9+.-])file:/i; function fieldPath(base: string, key: string | number): string { return base ? `${base}.${String(key)}` : String(key); @@ -80,6 +82,8 @@ export function enforceEventStructureLimits( } if ( /^[A-Za-z]:\\/.test(value) || + FILE_URI_RE.test(value) || + FILE_URI_RE.test(value.replace(ASCII_URL_WHITESPACE_RE, "")) || RAW_POSIX_PATH_RE.test(value) || value.includes("\\Users\\") ) { diff --git a/tests/lab-post-merge-hardening.test.ts b/tests/lab-post-merge-hardening.test.ts index 89b163fa62..695b5526e1 100644 --- a/tests/lab-post-merge-hardening.test.ts +++ b/tests/lab-post-merge-hardening.test.ts @@ -180,7 +180,7 @@ test("replay discards an oversized unterminated line after reporting it once", ( expect(replay.corruptions[0]?.kind).toBe("malformed_line"); }); -test("event privacy admission rejects raw POSIX path bypass forms", () => { +test("event privacy admission rejects raw filesystem path bypass forms", () => { for (const detail of [ "config=/home/alice/work/repo", "cwd=/usr/local/bin", @@ -192,6 +192,22 @@ test("event privacy admission rejects raw POSIX path bypass forms", () => { "cwd=/home/@alice", "cwd=/home/josé/work", "x-/home/alice", + "file:///etc/passwd", + "FiLe:///home/alice/secret.txt", + "file://localhost/home/alice/secret.txt", + "detail_file:///etc/passwd", + "file://server/share/secret", + String.raw`file:\\server\share\secret`, + String.raw`file:\C:\secret\data`, + String.raw`file:C:\private\secret`, + "file:etc/passwd", + "fi\nle:///etc/passwd", + "fil\te:///etc/passwd", + "file\r:///etc/passwd", + "file\n:///etc/passwd", + "detail\nfile:///etc/passwd", + "detail\rfile:etc/passwd", + "detail\tfile:C:\\private\\secret", ]) { try { enforceEventStructureLimits({ detail }); @@ -200,6 +216,10 @@ test("event privacy admission rejects raw POSIX path bypass forms", () => { expect((err as { code?: string }).code).toBe("raw_path"); } } + + for (const detail of ["https://example.com/path", "profile:///etc/passwd"]) { + expect(() => enforceEventStructureLimits({ detail })).not.toThrow(); + } }); test("invalid JSON contract artifacts classify as artifact_mismatch", () => {