mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-07-27 22:38:30 +08:00
`validateBranchName` rejects branch names containing a comma, even though `git check-ref-format` permits commas and GitHub itself accepts them. PRs whose head branch contains a `,` fail validation in-process before any git operation, so the action errors out immediately. Branch names with commas show up in real workflows when names are derived from titles, place names, or external identifiers (e.g. "feature/paris,france"). There is no workaround other than renaming the branch, which is often not under the user's control. All git calls in this file use execFileSync with an argv array, so no shell interpretation occurs and `,` carries no injection risk. This is the same reasoning used to add `#` in #1167 and `+` in #1248. - Add `,` to the validateBranchName whitelist regex - Update the surrounding comment and error message to match - Add a test case covering commas in title-derived branch names Fixes #1300
230 lines
9.0 KiB
TypeScript
230 lines
9.0 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();
|
|
});
|
|
|
|
it("should accept branch names containing # (git-valid, common in issue-linked branches)", () => {
|
|
// Reported in #1137: branches like "put-back-arm64-#2" were rejected
|
|
expect(() => validateBranchName("put-back-arm64-#2")).not.toThrow();
|
|
expect(() =>
|
|
validateBranchName("feature/#123-description"),
|
|
).not.toThrow();
|
|
expect(() => validateBranchName("fix/issue-#42")).not.toThrow();
|
|
});
|
|
|
|
it("should accept branch names containing + (generated by Claude Code EnterWorktree)", () => {
|
|
// EnterWorktree converts "/" in worktree names to "+" when generating branch names.
|
|
// e.g. EnterWorktree("feat/skill-consolidation") → branch "worktree-feat+skill-consolidation"
|
|
expect(() =>
|
|
validateBranchName("worktree-feat+skill-consolidation"),
|
|
).not.toThrow();
|
|
expect(() => validateBranchName("fix+issue-123")).not.toThrow();
|
|
expect(() => validateBranchName("feature+new-thing")).not.toThrow();
|
|
});
|
|
|
|
it("should accept branch names containing , (git-valid, common in title-derived branches)", () => {
|
|
// Reported in #1300: branches like "feature/a,b" were rejected, even though
|
|
// git check-ref-format and GitHub both accept commas. Common when branch names
|
|
// are derived from titles, place names, or external identifiers.
|
|
expect(() => validateBranchName("feature/a,b")).not.toThrow();
|
|
expect(() => validateBranchName("feature/paris,france")).not.toThrow();
|
|
expect(() => validateBranchName("fix/issue-1,2,3")).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();
|
|
});
|
|
});
|
|
});
|