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
70 changes: 70 additions & 0 deletions apps/server/src/resources/impls/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,48 @@ import path from 'node:path';
import os from 'node:os';

import { loadGitResource } from './git.ts';
import { GitResourceSchema } from '../schema.ts';
import type { BtcaGitResourceArgs } from '../types.ts';

describe('Branch name validation', () => {
const validGitResource = (branch: string) => ({
type: 'git' as const,
name: 'test',
url: 'https://github.com/test/repo',
branch
});

it('accepts monorepo-style scoped tags', () => {
const result = GitResourceSchema.safeParse(validGitResource('@chakra-ui/[email protected]'));
expect(result.success).toBe(true);
});

it('accepts simple version tags', () => {
const result = GitResourceSchema.safeParse(validGitResource('v18.2.0'));
expect(result.success).toBe(true);
});

it('accepts standard branch names', () => {
const result = GitResourceSchema.safeParse(validGitResource('feature/my-branch'));
expect(result.success).toBe(true);
});

it('rejects branch names starting with hyphen', () => {
const result = GitResourceSchema.safeParse(validGitResource('-dangerous'));
expect(result.success).toBe(false);
});

it('rejects branch names with spaces', () => {
const result = GitResourceSchema.safeParse(validGitResource('my branch'));
expect(result.success).toBe(false);
});

it('rejects branch names with special characters', () => {
const result = GitResourceSchema.safeParse(validGitResource('branch;rm -rf'));
expect(result.success).toBe(false);
});
});

describe('Git Resource', () => {
let testDir: string;

Expand Down Expand Up @@ -101,6 +141,36 @@ describe('Git Resource', () => {
expect(loadGitResource(args)).rejects.toThrow('Branch name must contain only');
});

it('throws error for branch name starting with hyphen', async () => {
const args: BtcaGitResourceArgs = {
type: 'git',
name: 'hyphen-branch',
url: 'https://github.com/test/repo',
branch: '-dangerous',
repoSubPaths: [],
resourcesDirectoryPath: testDir,
specialAgentInstructions: '',
quiet: true
};

expect(loadGitResource(args)).rejects.toThrow("must not start with '-'");
});

it('throws error for branch name with spaces', async () => {
const args: BtcaGitResourceArgs = {
type: 'git',
name: 'space-branch',
url: 'https://github.com/test/repo',
branch: 'my branch',
repoSubPaths: [],
resourcesDirectoryPath: testDir,
specialAgentInstructions: '',
quiet: true
};

expect(loadGitResource(args)).rejects.toThrow('Branch name must contain only');
});

it('throws error for path traversal attempt', async () => {
const args: BtcaGitResourceArgs = {
type: 'git',
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/resources/impls/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ const gitClone = async (args: {
if (!branchValidation.success) {
throw new ResourceError({
message: branchValidation.error,
hint: 'Branch names can only contain letters, numbers, hyphens, underscores, dots, and forward slashes.',
hint: 'Branch names can only contain letters, numbers, hyphens, underscores, dots, forward slashes, and @ symbols.',
cause: new Error('Branch validation failed')
});
}
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/resources/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const RESOURCE_NAME_REGEX = /^@?[a-zA-Z0-9][a-zA-Z0-9._-]*(\/[a-zA-Z0-9][a-zA-Z0
* Branch name: alphanumeric, forward slashes, dots, underscores, and hyphens.
* Must not start with hyphen to prevent git option injection.
*/
const BRANCH_NAME_REGEX = /^[a-zA-Z0-9/_.-]+$/;
const BRANCH_NAME_REGEX = /^[a-zA-Z0-9/_.\-@]+$/;

const parseUrl = (value: string) =>
Result.try(() => new URL(value)).match({
Expand Down Expand Up @@ -99,7 +99,7 @@ const BranchNameSchema = z
.max(LIMITS.BRANCH_NAME_MAX, `Branch name too long (max ${LIMITS.BRANCH_NAME_MAX} chars)`)
.regex(
BRANCH_NAME_REGEX,
'Branch name must contain only alphanumeric characters, forward slashes, dots, underscores, and hyphens'
'Branch name must contain only alphanumeric characters, forward slashes, dots, underscores, hyphens, and @ symbols'
)
.refine((branch) => !branch.startsWith('-'), {
message: "Branch name must not start with '-' to prevent git option injection"
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const RESOURCE_NAME_REGEX = /^@?[a-zA-Z0-9][a-zA-Z0-9._-]*(\/[a-zA-Z0-9][a-zA-Z0
* Branch name: alphanumeric, forward slashes, dots, underscores, and hyphens.
* Must not start with hyphen to prevent git option injection.
*/
const BRANCH_NAME_REGEX = /^[a-zA-Z0-9/_.-]+$/;
const BRANCH_NAME_REGEX = /^[a-zA-Z0-9/_.\-@]+$/;

/**
* Provider/Model names: letters, numbers, dots, underscores, plus, hyphens, forward slashes, colons.
Expand Down Expand Up @@ -152,7 +152,7 @@ export const validateBranchName = (branch: string): ValidationResult => {

if (!BRANCH_NAME_REGEX.test(branch)) {
return fail(
`Invalid branch name: "${branch}". Must contain only alphanumeric characters, forward slashes, dots, underscores, and hyphens`
`Invalid branch name: "${branch}". Must contain only alphanumeric characters, forward slashes, dots, underscores, hyphens, and @ symbols`
);
}

Expand Down
3 changes: 2 additions & 1 deletion apps/web/static/btca.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@
},
"branch": {
"type": "string",
"description": "Git branch to use",
"description": "Git branch, tag, or ref to use (supports monorepo-style tags like @scope/pkg@version)",
"pattern": "^[a-zA-Z0-9/_\\.\\-@]+$",
"default": "main"
},
"searchPath": {
Expand Down