Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/create-prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ function getCommitInstructions(
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
: ""
}
- Push to the remote: Bash(git push origin ${branchName})`;
- Push to the remote: Bash(git push origin "${branchName}")`;
}
}
}
Expand Down
18 changes: 11 additions & 7 deletions src/github/operations/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,19 @@ function extractFirstLabel(githubData: FetchDataResult): string | undefined {
}

/**
* Validates a git branch name against a strict whitelist pattern.
* Validates a git branch name against a whitelist pattern.
* This prevents command injection by ensuring only safe characters are used.
*
* Valid branch names:
* - Start with alphanumeric character (not dash, to prevent option injection)
* - Contain only alphanumeric, forward slash, hyphen, underscore, or period
* - Start with letter or number (not dash, to prevent option injection)
* - Contain only letters, numbers, forward slash, hyphen, underscore, period, @, or #
* - Support Unicode characters (e.g., Japanese branch names)
* - Do not start or end with a period
* - Do not end with a slash
* - Do not contain '..' (path traversal)
* - Do not contain '//' (consecutive slashes)
* - Do not end with '.lock'
* - Do not contain '@{'
* - Do not contain '@{' (git reflog syntax)
* - Do not contain control characters or special git characters (~^:?*[\])
*/
export function validateBranchName(branchName: string): void {
Expand All @@ -58,12 +59,15 @@ export function validateBranchName(branchName: string): void {
);
}

// Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$/;
// Whitelist pattern: Unicode letter/number start, then Unicode letter/number/slash/hyphen/underscore/period/@/#
// Allows @ and # for Azure DevOps integration (AB#123) and common naming patterns
// Allows Unicode for internationalization (e.g., Japanese branch names)
// Note: quotes are still blocked for safe shell interpolation
const validPattern = /^[\p{L}\p{N}][\p{L}\p{N}/_@#.-]*$/u;

if (!validPattern.test(branchName)) {
throw new Error(
`Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, or periods.`,
`Invalid branch name: "${branchName}". Branch names must start with a letter or number and contain only letters, numbers, forward slashes, hyphens, underscores, periods, @, or #.`,
);
}

Expand Down
44 changes: 44 additions & 0 deletions test/branch-template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,5 +243,49 @@ describe("branch template utilities", () => {
expect(result).toMatch(/^fix\/pr-456-\d{8}-\d{4}$/);
expect(result.length).toBeLessThanOrEqual(50);
});

it.each([
{
template: "{{prefix}}TICKET@{{entityNumber}}",
prefix: "feat/",
entityNumber: 123,
expected: "feat/TICKET@123",
description: "@ character",
},
{
template: "{{prefix}}AB#{{entityNumber}}-fix",
prefix: "feature/",
entityNumber: 1992,
expected: "feature/AB#1992-fix",
description: "# character",
},
{
template: "{{prefix}}機能-{{entityNumber}}",
prefix: "feat/",
entityNumber: 456,
expected: "feat/機能-456",
description: "Japanese characters",
},
{
template: "{{prefix}}особенность-{{entityNumber}}",
prefix: "fix/",
entityNumber: 789,
expected: "fix/особенность-789",
description: "Russian characters",
},
{
template: "{{prefix}}AB#{{entityNumber}}@新機能",
prefix: "特徴/",
entityNumber: 999,
expected: "特徴/AB#999@新機能",
description: "@, #, and Unicode combined",
},
])("should generate and validate branch name with $description", ({ template, prefix, entityNumber, expected }) => {
const result = generateBranchName(template, prefix, "issue", entityNumber);
expect(result).toBe(expected);

const { validateBranchName } = require("../src/github/operations/branch");
expect(() => validateBranchName(result)).not.toThrow();
});
});
});
14 changes: 14 additions & 0 deletions test/validate-branch-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ describe("validateBranchName", () => {
expect(() => validateBranchName("refs/heads/main")).not.toThrow();
expect(() => validateBranchName("bugfix/JIRA-1234")).not.toThrow();
});

it.each([
"TICKET-123@add-feature",
"user@branch",
"feat@test",
"feature/AB#1992-sentry-enhancements",
"AB#123-fix",
"issue#456",
"feat/add-機能追加",
"フィーチャー/新機能",
"особенность/новая",
])("should accept special characters and Unicode: %s", (branchName) => {
expect(() => validateBranchName(branchName)).not.toThrow();
});
});

describe("command injection attempts", () => {
Expand Down