fix(glob): fix typo that caused patterns like .*/* to escape cwd boundary (#24939)

## Summary

- Fixed a typo in `makeComponent` that incorrectly identified
2-character patterns starting with `.` (like `.*`) as `..` (DotBack)
patterns
- The condition checked `pattern[component.start] == '.'` twice instead
of checking both characters at positions 0 and 1
- This caused patterns like `.*/*` to be parsed as `../` + `*`, making
the glob walker traverse into parent directories

Fixes #24936

## Test plan

- [x] Added tests in `test/js/bun/glob/scan.test.ts` that verify
patterns like `.*/*` and `.*/**/*.ts` don't escape the cwd boundary
- [x] Tests fail with system bun (bug reproduced) and pass with the fix
- [x] All existing glob tests pass (169 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
robobun
2025-11-23 01:41:17 -08:00
committed by GitHub
parent 67be07fca4
commit 7076fbbe68
2 changed files with 74 additions and 2 deletions

View File

@@ -1431,12 +1431,12 @@ pub fn GlobWalker_(
if (component.len == 0) return null;
out: {
if (component.len == 1 and pattern[component.start] == '.') {
if (bun.strings.eqlComptime(pattern[component.start .. component.start + component.len], ".")) {
component.syntax_hint = .Dot;
has_relative_patterns.* = true;
break :out;
}
if (component.len == 2 and pattern[component.start] == '.' and pattern[component.start] == '.') {
if (bun.strings.eqlComptime(pattern[component.start .. component.start + component.len], "..")) {
component.syntax_hint = .DotBack;
has_relative_patterns.* = true;
break :out;

View File

@@ -665,6 +665,78 @@ describe("absolute path pattern", async () => {
});
});
// https://github.com/oven-sh/bun/issues/24936
describe("glob scan should not escape cwd boundary", () => {
test("pattern .*/* should not match parent directory via ..", async () => {
// Create a directory structure where we can verify paths don't escape cwd
const tempdir = tempDirWithFiles("glob-cwd-escape", {
".hidden": {
"file.txt": "hidden file content",
},
".dotfile": "dot file",
"regular": {
"file.txt": "regular file",
},
});
const glob = new Glob(".*/*");
const entries = await Array.fromAsync(
glob.scan({
cwd: tempdir,
onlyFiles: false,
dot: true, // Need dot:true to match dotfiles/directories
}),
);
// All entries should be within the cwd - none should start with ../
for (const entry of entries) {
expect(entry.startsWith("../")).toBe(false);
expect(entry.startsWith("..\\")).toBe(false);
expect(entry.includes("/../")).toBe(false);
expect(entry.includes("\\..\\")).toBe(false);
}
// Should match .hidden/file.txt but not escape to parent
expect(entries.sort()).toEqual([`.hidden${path.sep}file.txt`].sort());
});
test("pattern .*/**/*.ts should not escape cwd", async () => {
const tempdir = tempDirWithFiles("glob-cwd-escape-ts", {
".config": {
"settings.ts": "export default {}",
"nested": {
"deep.ts": "export const x = 1",
},
},
"src": {
"index.ts": "console.log('hi')",
},
});
const glob = new Glob(".*/**/*.ts");
const entries = await Array.fromAsync(
glob.scan({
cwd: tempdir,
onlyFiles: true,
dot: true, // Need dot:true to match dotfiles/directories
}),
);
// All entries should be within the cwd
for (const entry of entries) {
expect(entry.startsWith("../")).toBe(false);
expect(entry.startsWith("..\\")).toBe(false);
expect(entry.includes("/../")).toBe(false);
expect(entry.includes("\\..\\")).toBe(false);
}
// Should match files in .config but not escape to parent
expect(entries.sort()).toEqual(
[`.config${path.sep}settings.ts`, `.config${path.sep}nested${path.sep}deep.ts`].sort(),
);
});
});
describe("glob.scan wildcard fast path", async () => {
test("works", async () => {
const tempdir = tempDirWithFiles("glob-scan-wildcard-fast-path", {