Skip to content

Commit 7980a30

Browse files
committed
fix(secret-guard): classify remote URL glob schemes
1 parent 3a58cf0 commit 7980a30

3 files changed

Lines changed: 261 additions & 62 deletions

File tree

‎src/permission/auto-shell-policy.test.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const shellCall = (command: string): ToolCall => ({
88
arguments: { command },
99
});
1010

11-
describe("local file URL brace expansion", () => {
11+
describe("local file URL glob expansion", () => {
1212
const localBraceURLs = [
1313
"file:///tmp/{%2Eenv,README.md}",
1414
"file:///tmp/{README.md,%2Eenv}",
@@ -40,6 +40,12 @@ describe("local file URL brace expansion", () => {
4040
"{https://example.com/README.md,file:///tmp/%2Eenv}",
4141
"f{i,oo}{le,tp}:///tmp/%2Eenv",
4242
"F{ILE,OO}:///tmp/%2Eenv",
43+
"f[i-i]le:///tmp/%2Eenv",
44+
"f[a-z]le:///tmp/%2Eenv",
45+
"F[I-I]LE:///tmp/%2Eenv",
46+
"f[i-i]l{e,x}:///tmp/%2Eenv",
47+
"f[i]le:///tmp/%2Eenv",
48+
"f[i-i le:///tmp/%2Eenv",
4349
];
4450
const commands = synthesizedURLs.flatMap((url) => [
4551
`curl '${url}'`,
@@ -57,14 +63,27 @@ describe("local file URL brace expansion", () => {
5763
});
5864

5965
test("does not apply the local brace rule to remote URLs", () => {
66+
const remoteSchemes: string[] = Array.from({ length: 800 }, (_, index) =>
67+
index % 2 === 0 ? "https" : "http",
68+
);
69+
const overlengthRemote = `{${remoteSchemes.join(",")}}://example.com/{one,two}`;
6070
for (const url of [
6171
"https://example.com/{one,two}",
6272
"https://example.com/%7Bone,two%7D",
6373
"https://example.com/{one",
6474
"https://example.com/two}",
75+
"h[t-t]tp://example.com/[a-z]",
76+
"https://example.com/[a-z]?q=[0-9]",
77+
overlengthRemote,
6578
]) {
6679
expect(autoShellRuleForCall(shellCall(`curl '${url}'`))).toBeUndefined();
6780
}
81+
82+
remoteSchemes[799] = "file";
83+
const overlengthFileCapable = `{${remoteSchemes.join(",")}}:///tmp/%2Eenv`;
84+
expect(
85+
autoShellRuleForCall(shellCall(`curl '${overlengthFileCapable}'`)),
86+
).toMatchObject({ name: "sensitive-path", effect: "ask" });
6887
});
6988
});
7089

‎src/plugins/secret-guard-plugin.test.ts‎

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ describe("secret-guard file URL normalization", () => {
438438
);
439439
}
440440

441-
const schemeBraceURLs = [
441+
const synthesizedSchemeURLs = [
442442
"{file,https}:///tmp/%2Eenv",
443443
"{https,file}:///tmp/%2Eenv",
444444
"file{,s}:///tmp/%2Eenv",
@@ -449,9 +449,13 @@ describe("secret-guard file URL normalization", () => {
449449
"{https://127.0.0.1:1/README.md,file:///tmp/%2Eenv}",
450450
"f{i,oo}{le,tp}:///tmp/%2Eenv",
451451
"F{ILE,OO}:///tmp/%2Eenv",
452+
"f[i-i]le:///tmp/%2Eenv",
453+
"f[a-z]le:///tmp/%2Eenv",
454+
"F[I-I]LE:///tmp/%2Eenv",
455+
"f[i-i]l{e,x}:///tmp/%2Eenv",
452456
];
453457

454-
for (const url of schemeBraceURLs) {
458+
for (const url of synthesizedSchemeURLs) {
455459
test.skipIf(Bun.which("curl") === null)(
456460
`flags curl scheme synthesis that reads a real .env: ${url}`,
457461
async () => {
@@ -483,31 +487,84 @@ describe("secret-guard file URL normalization", () => {
483487
);
484488
}
485489

486-
test("keeps remote-only scheme braces allowed", () => {
490+
test("keeps remote-only scheme globs allowed", () => {
487491
const overflow = `{${Array.from({ length: 80 }, (_, index) =>
488492
index % 2 === 0 ? "https" : "http",
489493
).join(",")}}://example.com/{one,two}`;
494+
const overlength = `{${Array.from({ length: 800 }, (_, index) =>
495+
index % 2 === 0 ? "https" : "http",
496+
).join(",")}}://example.com/{one,two}`;
490497
for (const url of [
491498
"{https,http}://example.com/{one,two}",
492499
"{http,https}://example.com/{one,two}",
493500
"h{ttp,ttps}://example.com/{one,two}",
501+
"{{https,http},{http,https}}://example.com/{one,two}",
502+
"h{ttp,ttps}{,s}://example.com/{one,two}",
503+
"h[t-t]tp://example.com/[a-z]",
504+
"https://example.com/[a-z]?q=[0-9]",
494505
overflow,
506+
overlength,
495507
`https://example.com/${"x".repeat(4_096)}/{one,two}`,
496508
]) {
497509
expect(commandReferencesSensitivePath(`curl '${url}'`)).toBeUndefined();
498510
}
499511
});
500512

501-
test("fails closed for ambiguous and overflowing file-scheme braces", () => {
513+
test("classifies 1000 remote-only bracket URLs within a bounded time", () => {
514+
const url = `f[t-t]p://example.com/${"[a-z]".repeat(1_000)}`;
515+
const started = performance.now();
516+
for (let index = 0; index < 1_000; index++) {
517+
expect(commandReferencesSensitivePath(`curl '${url}'`)).toBeUndefined();
518+
}
519+
expect(performance.now() - started).toBeLessThan(1_000);
520+
});
521+
522+
test("fails closed for file-capable scheme braces in any position", () => {
523+
const remote: string[] = Array.from({ length: 800 }, (_, index) =>
524+
index % 2 === 0 ? "https" : "http",
525+
);
526+
const withFileAt = (index: number) => {
527+
const schemes = [...remote];
528+
schemes[index] = "file";
529+
return `{${schemes.join(",")}}:///tmp/%2Eenv`;
530+
};
531+
for (const url of [
532+
withFileAt(0),
533+
withFileAt(400),
534+
withFileAt(799),
535+
"{{https,http},{ftp,file}}:///tmp/%2Eenv",
536+
"{f,h}{ile,ttps}:///tmp/%2Eenv",
537+
]) {
538+
expect(commandReferencesSensitivePath(`curl '${url}'`)).toBeDefined();
539+
}
540+
});
541+
542+
test("classifies a 100x remote-only alternative list within a bounded time", () => {
543+
const url = `{${Array.from({ length: 80_000 }, (_, index) =>
544+
index % 2 === 0 ? "https" : "http",
545+
).join(",")}}://example.com/{one,two}`;
546+
const started = performance.now();
547+
expect(commandReferencesSensitivePath(`curl '${url}'`)).toBeUndefined();
548+
expect(performance.now() - started).toBeLessThan(1_000);
549+
});
550+
551+
test("fails closed for ambiguous and overflowing file-scheme globs", () => {
502552
const overflow = `f{${Array.from({ length: 80 }, (_, index) =>
503553
index === 79 ? "ile" : `x${index}`,
504554
).join(",")}}:///tmp/%2Eenv`;
505555
const overlength = `f{ile,${"x".repeat(4_096)}}:///tmp/%2Eenv`;
556+
const malformedOverlength = `{${Array.from({ length: 800 }, (_, index) =>
557+
index === 799 ? "f{ile" : "https",
558+
).join(",")}:///tmp/%2Eenv`;
506559
for (const url of [
507560
"f{ile:,https:///tmp/%2Eenv",
508561
"f{i,{oo,ILE}}:///tmp/%2Eenv",
562+
"f[i]le:///tmp/%2Eenv",
563+
"f[i-i le:///tmp/%2Eenv",
564+
"f[i,i]le:///tmp/%2Eenv",
509565
overflow,
510566
overlength,
567+
malformedOverlength,
511568
]) {
512569
expect(commandReferencesSensitivePath(`curl '${url}'`)).toBeDefined();
513570
}

0 commit comments

Comments
 (0)