mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
* fix: Prevent command injection in branch operations Replace Bun shell template literals with Node.js execFileSync to prevent command injection attacks via malicious branch names. Branch names from PR data (headRefName) are now validated against a strict whitelist pattern before use in git commands. Changes: - Add validateBranchName() function with strict character whitelist - Replace $`git ...` shell templates with execGit() using execFileSync - Validate all branch names before use in git operations * fix: Address review comments for branch validation security - Enhanced execGit JSDoc to explain security benefits of execFileSync - Added comprehensive branch name validation: - Leading dash check (prevents option injection) - Control characters and special git characters (~^:?*[\]) - Leading/trailing period checks - Trailing slash and consecutive slash checks - Added -- separator to git checkout commands - Added 30 unit tests for validateBranchName covering: - Valid branch names - Command injection attempts - Option injection attempts - Path traversal attempts - Git-specific invalid patterns - Control characters and edge cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
202 lines
7.5 KiB
TypeScript
202 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import { validateBranchName } from "../src/github/operations/branch";
|
|
|
|
describe("validateBranchName", () => {
|
|
describe("valid branch names", () => {
|
|
it("should accept simple alphanumeric names", () => {
|
|
expect(() => validateBranchName("main")).not.toThrow();
|
|
expect(() => validateBranchName("feature123")).not.toThrow();
|
|
expect(() => validateBranchName("Branch1")).not.toThrow();
|
|
});
|
|
|
|
it("should accept names with hyphens", () => {
|
|
expect(() => validateBranchName("feature-branch")).not.toThrow();
|
|
expect(() => validateBranchName("fix-bug-123")).not.toThrow();
|
|
});
|
|
|
|
it("should accept names with underscores", () => {
|
|
expect(() => validateBranchName("feature_branch")).not.toThrow();
|
|
expect(() => validateBranchName("fix_bug_123")).not.toThrow();
|
|
});
|
|
|
|
it("should accept names with forward slashes", () => {
|
|
expect(() => validateBranchName("feature/new-thing")).not.toThrow();
|
|
expect(() => validateBranchName("user/feature/branch")).not.toThrow();
|
|
});
|
|
|
|
it("should accept names with periods", () => {
|
|
expect(() => validateBranchName("v1.0.0")).not.toThrow();
|
|
expect(() => validateBranchName("release.1.2.3")).not.toThrow();
|
|
});
|
|
|
|
it("should accept typical branch name formats", () => {
|
|
expect(() =>
|
|
validateBranchName("claude/issue-123-20250101-1234"),
|
|
).not.toThrow();
|
|
expect(() => validateBranchName("refs/heads/main")).not.toThrow();
|
|
expect(() => validateBranchName("bugfix/JIRA-1234")).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("command injection attempts", () => {
|
|
it("should reject shell command substitution with $()", () => {
|
|
expect(() => validateBranchName("$(whoami)")).toThrow();
|
|
expect(() => validateBranchName("branch-$(rm -rf /)")).toThrow();
|
|
expect(() => validateBranchName("test$(cat /etc/passwd)")).toThrow();
|
|
});
|
|
|
|
it("should reject shell command substitution with backticks", () => {
|
|
expect(() => validateBranchName("`whoami`")).toThrow();
|
|
expect(() => validateBranchName("branch-`rm -rf /`")).toThrow();
|
|
});
|
|
|
|
it("should reject command chaining with semicolons", () => {
|
|
expect(() => validateBranchName("branch; rm -rf /")).toThrow();
|
|
expect(() => validateBranchName("test;whoami")).toThrow();
|
|
});
|
|
|
|
it("should reject command chaining with &&", () => {
|
|
expect(() => validateBranchName("branch && rm -rf /")).toThrow();
|
|
expect(() => validateBranchName("test&&whoami")).toThrow();
|
|
});
|
|
|
|
it("should reject command chaining with ||", () => {
|
|
expect(() => validateBranchName("branch || rm -rf /")).toThrow();
|
|
expect(() => validateBranchName("test||whoami")).toThrow();
|
|
});
|
|
|
|
it("should reject pipe characters", () => {
|
|
expect(() => validateBranchName("branch | cat")).toThrow();
|
|
expect(() => validateBranchName("test|grep password")).toThrow();
|
|
});
|
|
|
|
it("should reject redirection operators", () => {
|
|
expect(() => validateBranchName("branch > /etc/passwd")).toThrow();
|
|
expect(() => validateBranchName("branch < input")).toThrow();
|
|
expect(() => validateBranchName("branch >> file")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("option injection attempts", () => {
|
|
it("should reject branch names starting with dash", () => {
|
|
expect(() => validateBranchName("-x")).toThrow(
|
|
/cannot start with a dash/,
|
|
);
|
|
expect(() => validateBranchName("--help")).toThrow(
|
|
/cannot start with a dash/,
|
|
);
|
|
expect(() => validateBranchName("-")).toThrow(/cannot start with a dash/);
|
|
expect(() => validateBranchName("--version")).toThrow(
|
|
/cannot start with a dash/,
|
|
);
|
|
expect(() => validateBranchName("-rf")).toThrow(
|
|
/cannot start with a dash/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("path traversal attempts", () => {
|
|
it("should reject double dot sequences", () => {
|
|
expect(() => validateBranchName("../../../etc")).toThrow();
|
|
expect(() => validateBranchName("branch/../secret")).toThrow(/'\.\.'$/);
|
|
expect(() => validateBranchName("a..b")).toThrow(/'\.\.'$/);
|
|
});
|
|
});
|
|
|
|
describe("git-specific invalid patterns", () => {
|
|
it("should reject @{ sequence", () => {
|
|
expect(() => validateBranchName("branch@{1}")).toThrow(/@{/);
|
|
expect(() => validateBranchName("HEAD@{yesterday}")).toThrow(/@{/);
|
|
});
|
|
|
|
it("should reject .lock suffix", () => {
|
|
expect(() => validateBranchName("branch.lock")).toThrow(/\.lock/);
|
|
expect(() => validateBranchName("feature.lock")).toThrow(/\.lock/);
|
|
});
|
|
|
|
it("should reject consecutive slashes", () => {
|
|
expect(() => validateBranchName("feature//branch")).toThrow(
|
|
/consecutive slashes/,
|
|
);
|
|
expect(() => validateBranchName("a//b//c")).toThrow(
|
|
/consecutive slashes/,
|
|
);
|
|
});
|
|
|
|
it("should reject trailing slashes", () => {
|
|
expect(() => validateBranchName("feature/")).toThrow(
|
|
/cannot end with a slash/,
|
|
);
|
|
expect(() => validateBranchName("branch/")).toThrow(
|
|
/cannot end with a slash/,
|
|
);
|
|
});
|
|
|
|
it("should reject leading periods", () => {
|
|
expect(() => validateBranchName(".hidden")).toThrow();
|
|
});
|
|
|
|
it("should reject trailing periods", () => {
|
|
expect(() => validateBranchName("branch.")).toThrow(
|
|
/cannot start or end with a period/,
|
|
);
|
|
});
|
|
|
|
it("should reject special git refspec characters", () => {
|
|
expect(() => validateBranchName("branch~1")).toThrow();
|
|
expect(() => validateBranchName("branch^2")).toThrow();
|
|
expect(() => validateBranchName("branch:ref")).toThrow();
|
|
expect(() => validateBranchName("branch?")).toThrow();
|
|
expect(() => validateBranchName("branch*")).toThrow();
|
|
expect(() => validateBranchName("branch[0]")).toThrow();
|
|
expect(() => validateBranchName("branch\\path")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("control characters and special characters", () => {
|
|
it("should reject null bytes", () => {
|
|
expect(() => validateBranchName("branch\x00name")).toThrow();
|
|
});
|
|
|
|
it("should reject other control characters", () => {
|
|
expect(() => validateBranchName("branch\x01name")).toThrow();
|
|
expect(() => validateBranchName("branch\x1Fname")).toThrow();
|
|
expect(() => validateBranchName("branch\x7Fname")).toThrow();
|
|
});
|
|
|
|
it("should reject spaces", () => {
|
|
expect(() => validateBranchName("branch name")).toThrow();
|
|
expect(() => validateBranchName("feature branch")).toThrow();
|
|
});
|
|
|
|
it("should reject newlines and tabs", () => {
|
|
expect(() => validateBranchName("branch\nname")).toThrow();
|
|
expect(() => validateBranchName("branch\tname")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("empty and whitespace", () => {
|
|
it("should reject empty strings", () => {
|
|
expect(() => validateBranchName("")).toThrow(/cannot be empty/);
|
|
});
|
|
|
|
it("should reject whitespace-only strings", () => {
|
|
expect(() => validateBranchName(" ")).toThrow();
|
|
expect(() => validateBranchName("\t\n")).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("edge cases", () => {
|
|
it("should accept single alphanumeric character", () => {
|
|
expect(() => validateBranchName("a")).not.toThrow();
|
|
expect(() => validateBranchName("1")).not.toThrow();
|
|
});
|
|
|
|
it("should reject single special characters", () => {
|
|
expect(() => validateBranchName(".")).toThrow();
|
|
expect(() => validateBranchName("/")).toThrow();
|
|
expect(() => validateBranchName("-")).toThrow();
|
|
});
|
|
});
|
|
});
|