Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
8485d71a1b fix(install): relax committish validation to allow git revision operators
The isValidCommittish function was applying git check-ref-format rules,
but a committish is a revision specifier, not a ref name. Characters like
~ (ancestor), ^ (parent/dereference), and : (tree path) are valid in
git revision syntax (e.g., v1.0^0, v1.0~2). The security goal only
requires blocking flag injection (leading -), control characters, and
spaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-28 09:25:51 +00:00
Claude Bot
6db4b71e13 refactor(test): extract helper and strengthen committish assertions
Extract runInstallWithCommittish helper to reduce boilerplate across
all 6 tests. Strengthen rejection assertions to check the full
diagnostic message including the committish value and package name
(e.g. 'invalid committish "--output=/tmp/pwn" for "is-number"').

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 06:47:16 +00:00
Claude Bot
29e3bf7dae fix(test): drain stdout pipe in committish validation tests
Consume proc.stdout.text() in Promise.all alongside stderr and exited
to prevent a filled pipe from blocking the child process.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 06:33:40 +00:00
Claude Bot
f88924367e fix(install): harden git dependency committish validation
Validate committish values in git dependencies against the git
ref naming rules before passing them to git commands. This ensures
that malformed or unexpected committish strings (e.g. starting with
'-' or '.', containing '..', control characters, etc.) are rejected
early with a clear error message rather than being passed through
to git subprocesses.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 06:22:00 +00:00
2 changed files with 97 additions and 0 deletions

View File

@@ -553,6 +553,30 @@ pub const Repository = extern struct {
};
}
/// Returns whether a committish string is safe to pass to git commands.
/// This is a security check to prevent flag injection, not a ref-name
/// validation. Git revision specifiers legitimately use operators like
/// `~` (ancestor), `^` (parent/dereference), and `:` (tree path), so
/// those are allowed. We block:
/// - leading `-` (would be interpreted as a git flag)
/// - control characters (< 0x20) and DEL (0x7f)
/// - spaces (not valid in committish values)
/// - NUL bytes (string terminator issues)
pub fn isValidCommittish(committish: string) bool {
if (committish.len == 0) return true;
// Must not start with '-' (flag injection)
if (committish[0] == '-') return false;
for (committish) |c| {
switch (c) {
// Block control characters, spaces, and DEL
' ', 0x7f => return false,
else => if (c < 0x20) return false,
}
}
return true;
}
pub fn findCommit(
allocator: std.mem.Allocator,
env: *DotEnv.Loader,
@@ -568,6 +592,17 @@ pub const Repository = extern struct {
_ = repo_dir;
if (committish.len > 0 and !isValidCommittish(committish)) {
log.addErrorFmt(
null,
logger.Loc.Empty,
allocator,
"invalid committish \"{s}\" for \"{s}\"",
.{ committish, name },
) catch unreachable;
return error.InvalidCommittish;
}
return std.mem.trim(u8, exec(
allocator,
shared_env.get(allocator, env),

View File

@@ -0,0 +1,62 @@
import { describe, expect, it, setDefaultTimeout } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
setDefaultTimeout(1000 * 60 * 5);
function envWithCache(dir: string) {
return { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(dir, ".bun-cache") };
}
// Use git+https://git@ format to force the git clone + findCommit path
// rather than the GitHub tarball download path.
const gitUrlBase = "git+https://git@github.com/jonschlinkert/is-number.git";
async function runInstallWithCommittish(dirName: string, committish: string, expected: { shouldReject: boolean }) {
using dir = tempDir(dirName, {
"package.json": JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
"is-number": `${gitUrlBase}#${committish}`,
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: envWithCache(String(dir)),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
if (expected.shouldReject) {
expect(stderr).toContain(`invalid committish "${committish}" for "is-number"`);
expect(exitCode).toBe(1);
} else {
expect(stderr).not.toContain("invalid committish");
expect(stderr).toContain("Saved lockfile");
expect(exitCode).toBe(0);
}
}
describe("git committish validation", () => {
it("should reject committish starting with a dash (flag injection)", async () => {
await runInstallWithCommittish("committish-dash", "--output=/tmp/pwn", { shouldReject: true });
});
it("should reject committish that is a single dash flag", async () => {
await runInstallWithCommittish("committish-flag", "-v", { shouldReject: true });
});
it("should accept valid committish with tag", async () => {
await runInstallWithCommittish("committish-valid-tag", "7.0.0", { shouldReject: false });
});
it("should accept valid committish with short commit hash", async () => {
await runInstallWithCommittish("committish-valid-hash", "98e8ff1", { shouldReject: false });
});
});