From ac2f9a13a07df22a17f86a1bfc78218c91fe5fef Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 22 Jun 2026 19:50:20 +0200 Subject: [PATCH 1/2] fix(engine): re-validate expanded brace patterns against safeGlob expandBracePattern() can produce absolute paths that bypass the initial safeGlob() check (e.g. `{/etc/passwd,foo}` starts with `{` so isAbsolute() returns false, but expands to `/etc/passwd`). Add safeGlob() validation on each expanded pattern before passing to Bun.Glob in both ctx.glob() and ctx.grepFiles(). Follow-up to #422, addressing CodeRabbit review feedback. Claude-Session: https://claude.ai/code/session_01C3VSm9YHmfhZ9kLw2fkLE4 Signed-off-by: Rhuan Barreto --- src/engine/runner.ts | 2 ++ tests/engine/runner-security.test.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) 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..a321641e 100644 --- a/tests/engine/runner-security.test.ts +++ b/tests/engine/runner-security.test.ts @@ -179,4 +179,23 @@ 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"); + }); }); From 076d51955806dd0ca8aad784460d3c4ed89ca1fb Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 22 Jun 2026 20:02:16 +0200 Subject: [PATCH 2/2] test: add grepFiles brace expansion denial test Covers the grepFiles() path in addition to the existing glob() test, ensuring both entry points re-validate expanded patterns via safeGlob(). Claude-Session: https://claude.ai/code/session_01C3VSm9YHmfhZ9kLw2fkLE4 Signed-off-by: Rhuan Barreto --- tests/engine/runner-security.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/engine/runner-security.test.ts b/tests/engine/runner-security.test.ts index a321641e..707acf3d 100644 --- a/tests/engine/runner-security.test.ts +++ b/tests/engine/runner-security.test.ts @@ -198,4 +198,24 @@ describe("runChecks path sandboxing", () => { 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"); + }); });