diff --git a/src/engine/runner.ts b/src/engine/runner.ts index 04134ce7..e747f5df 100644 --- a/src/engine/runner.ts +++ b/src/engine/runner.ts @@ -159,6 +159,7 @@ function createRuleContext( const patterns = expandBracePattern(pattern); const seen = new Set(); for (const p of patterns) { + safeGlob(p); const g = new Bun.Glob(p); // dot: true so rules can target dot-prefixed paths like `.github/`, // `.husky/`, `.vscode/` — first-class source dirs in code repos. @@ -205,6 +206,7 @@ function createRuleContext( // See https://github.com/archgate/cli/issues/222. const seen = new Set(); for (const p of globs) { + safeGlob(p); const g = new Bun.Glob(p); // oxlint-disable-next-line no-await-in-loop -- sequential scan per expanded brace alternative for await (const file of g.scan({ cwd: projectRoot, dot: true })) { diff --git a/tests/engine/runner-security.test.ts b/tests/engine/runner-security.test.ts index fc4d5ff1..707acf3d 100644 --- a/tests/engine/runner-security.test.ts +++ b/tests/engine/runner-security.test.ts @@ -179,4 +179,43 @@ describe("runChecks path sandboxing", () => { const result = await runChecks(tempDir, [loaded]); expect(result.results[0].error).toContain("escapes project root"); }); + + test("blocks absolute paths produced by brace expansion", async () => { + const loaded = makeLoadedAdr( + {}, + { + rules: { + "brace-abs-glob": { + description: "Attempt absolute path via brace expansion", + async check(ctx) { + await ctx.glob("{/etc/passwd,src/a.ts}"); + }, + }, + }, + } + ); + + const result = await runChecks(tempDir, [loaded]); + expect(result.results[0].error).toContain("access denied"); + }); + + test("blocks absolute paths produced by brace expansion in grepFiles", async () => { + const loaded = makeLoadedAdr( + {}, + { + rules: { + "brace-abs-grepfiles": { + description: + "Attempt absolute path via brace expansion in grepFiles", + async check(ctx) { + await ctx.grepFiles(/./u, "{/etc/passwd,src/a.ts}"); + }, + }, + }, + } + ); + + const result = await runChecks(tempDir, [loaded]); + expect(result.results[0].error).toContain("access denied"); + }); });