Skip to content

Commit 02076cb

Browse files
committed
fix(secret-guard): accept leading-zero curl range steps
1 parent 4c5b55a commit 02076cb

4 files changed

Lines changed: 74 additions & 4 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ describe("local file URL glob expansion", () => {
4242
"F{ILE,OO}:///tmp/%2Eenv",
4343
"f[i-i]le:///tmp/%2Eenv",
4444
"f[a-z]le:///tmp/%2Eenv",
45+
"f[a-z:02]le:///tmp/%2Eenv",
46+
"f[a-z:0002]le:///tmp/%2Eenv",
47+
"f[a-z:0]le:///tmp/%2Eenv",
4548
"F[I-I]LE:///tmp/%2Eenv",
4649
"f[i-i]l{e,x}:///tmp/%2Eenv",
4750
"f[i]le:///tmp/%2Eenv",
@@ -73,6 +76,9 @@ describe("local file URL glob expansion", () => {
7376
"https://example.com/{one",
7477
"https://example.com/two}",
7578
"h[t-t]tp://example.com/[a-z]",
79+
"f[a-z:3]le:///tmp/%2Eenv",
80+
"f[z-a:02]le:///tmp/%2Eenv",
81+
"h[t-z:02]tp://example.com/%2Eenv",
7682
"https://example.com/[a-z]?q=[0-9]",
7783
overlengthRemote,
7884
]) {

‎src/permission/classify-security.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,25 @@ describe("sensitive-path shell commands require approval, not a hard deny", () =
580580
expect(asked).toBe(1);
581581
});
582582

583+
test("stored curl grants do not authorize stepped file-scheme synthesis", async () => {
584+
let asked = 0;
585+
const gate = createPermissionGate({
586+
approvals: [{ tool: "run_shell", pattern: "curl *" }],
587+
requestApproval: async () => {
588+
asked++;
589+
return { allow: true };
590+
},
591+
interactive: true,
592+
skipPermissions: false,
593+
reactorGated: false,
594+
});
595+
const verdict = await gate.evaluate(
596+
shellCall("curl 'f[a-z:02]le:///tmp/%2Eenv'"),
597+
);
598+
expect(verdict.allowed).toBe(true);
599+
expect(asked).toBe(1);
600+
});
601+
583602
test("stored grants still authorize ordinary shell reads", async () => {
584603
let asked = 0;
585604
const gate = createPermissionGate({

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { join } from "node:path";
55
import { createPosixTools } from "@intx/tools-posix";
66
import type { ToolCall, ToolResult } from "@intx/types/runtime";
77
import { buildCorePosixToolPlugins } from "../agent/posix-tool-plugins.js";
8+
import { autoShellRuleForCall } from "../permission/auto-shell-policy.js";
89
import { createPermissionGate } from "../permission/gate.js";
910
import { loadProjectApprovals } from "../permission/store.js";
1011
import {
@@ -452,6 +453,8 @@ describe("secret-guard file URL normalization", () => {
452453
"F{ILE,OO}:///tmp/%2Eenv",
453454
"f[i-i]le:///tmp/%2Eenv",
454455
"f[a-z]le:///tmp/%2Eenv",
456+
"f[a-z:02]le:///tmp/%2Eenv",
457+
"f[a-z:0002]le:///tmp/%2Eenv",
455458
"F[I-I]LE:///tmp/%2Eenv",
456459
"f[i-i]l{e,x}:///tmp/%2Eenv",
457460
];
@@ -488,6 +491,41 @@ describe("secret-guard file URL normalization", () => {
488491
);
489492
}
490493

494+
test("auto mode asks for leading-zero curl ranges across wrappers", () => {
495+
for (const step of ["02", "0002"]) {
496+
const url = `f[a-z:${step}]le:///tmp/%2Eenv`;
497+
for (const command of [
498+
`curl '${url}'`,
499+
`env curl '${url}'`,
500+
`bash -c "curl '${url}'"`,
501+
`sh -c "curl '${url}'"`,
502+
]) {
503+
expect(autoShellRuleForCall(shell(command), () => false)?.name).toBe(
504+
"sensitive-path",
505+
);
506+
}
507+
}
508+
});
509+
510+
test("classifies stepped scheme ranges without widening remote URLs", () => {
511+
for (const url of [
512+
"f[a-z:2]le:///tmp/%2Eenv",
513+
"f[a-z:02]le:///tmp/%2Eenv",
514+
"f[a-z:0002]le:///tmp/%2Eenv",
515+
"f[a-z:0]le:///tmp/%2Eenv",
516+
"f[i]le:///tmp/%2Eenv",
517+
]) {
518+
expect(commandReferencesSensitivePath(`curl '${url}'`)).toBeDefined();
519+
}
520+
for (const url of [
521+
"f[a-z:3]le:///tmp/%2Eenv",
522+
"f[z-a:02]le:///tmp/%2Eenv",
523+
"h[t-z:02]tp://example.com/%2Eenv",
524+
]) {
525+
expect(commandReferencesSensitivePath(`curl '${url}'`)).toBeUndefined();
526+
}
527+
});
528+
491529
test("keeps remote-only scheme globs allowed", () => {
492530
const overflow = `{${Array.from({ length: 80 }, (_, index) =>
493531
index % 2 === 0 ? "https" : "http",
@@ -512,7 +550,7 @@ describe("secret-guard file URL normalization", () => {
512550
});
513551

514552
test("classifies 1000 remote-only bracket URLs within a bounded time", () => {
515-
const url = `f[t-t]p://example.com/${"[a-z]".repeat(1_000)}`;
553+
const url = `f[t-t:0002]p://example.com/${"[a-z:02]".repeat(1_000)}`;
516554
const started = performance.now();
517555
for (let index = 0; index < 1_000; index++) {
518556
expect(commandReferencesSensitivePath(`curl '${url}'`)).toBeUndefined();

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,9 +495,7 @@ interface RangeEmission {
495495
}
496496

497497
function rangeEmission(expression: string, expected: string): RangeEmission {
498-
const range = /^([A-Za-z0-9])-([A-Za-z0-9])(?::([1-9][0-9]*))?$/.exec(
499-
expression,
500-
);
498+
const range = /^([A-Za-z0-9])-([A-Za-z0-9])(?::([0-9]+))?$/.exec(expression);
501499
if (range === null) {
502500
const single = /^[A-Za-z0-9]$/.test(expression);
503501
return {
@@ -525,6 +523,15 @@ function rangeEmission(expression: string, expected: string): RangeEmission {
525523
expected.toLowerCase().codePointAt(0) ?? -1,
526524
expected.toUpperCase().codePointAt(0) ?? -1,
527525
];
526+
if (!Number.isInteger(step) || step <= 0) {
527+
return {
528+
includesExpected: expectedCodes.some(
529+
(code) => code >= startCode && code <= endCode,
530+
),
531+
includesOther: true,
532+
valid: false,
533+
};
534+
}
528535
const includesExpected = expectedCodes.some(
529536
(code) =>
530537
code >= startCode && code <= endCode && (code - startCode) % step === 0,

0 commit comments

Comments
 (0)