Implemented strict whitelist-based validation for git branch names and repository URLs to prevent command injection vulnerabilities in worktree operations. Security fixes: - Created git-validation.util.ts with whitelist validation functions - Added custom DTO validators for branch names and repository URLs - Applied defense-in-depth validation in WorktreeManagerService - Comprehensive test coverage (31 tests) for all validation scenarios Validation rules: - Branch names: alphanumeric + hyphens + underscores + slashes + dots only - Repository URLs: https://, http://, ssh://, git:// protocols only - Blocks: option injection (--), command substitution ($(), ``), shell operators - Prevents: SSRF attacks (localhost, internal networks), credential injection Defense layers: 1. DTO validation (first line of defense at API boundary) 2. Service-level validation (defense-in-depth before git operations) Fixes #274 Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
239 lines
9.5 KiB
TypeScript
239 lines
9.5 KiB
TypeScript
/**
|
|
* Git Validation Utility Tests
|
|
*
|
|
* Tests for command injection prevention in git operations
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { BadRequestException } from "@nestjs/common";
|
|
import {
|
|
validateBranchName,
|
|
validateRepositoryUrl,
|
|
validateSpawnContext,
|
|
} from "./git-validation.util";
|
|
|
|
describe("validateBranchName", () => {
|
|
describe("Valid branch names", () => {
|
|
it("should accept standard branch names", () => {
|
|
expect(() => validateBranchName("main")).not.toThrow();
|
|
expect(() => validateBranchName("develop")).not.toThrow();
|
|
expect(() => validateBranchName("master")).not.toThrow();
|
|
});
|
|
|
|
it("should accept feature branch names with slashes", () => {
|
|
expect(() => validateBranchName("feature/add-login")).not.toThrow();
|
|
expect(() => validateBranchName("fix/bug-123")).not.toThrow();
|
|
expect(() => validateBranchName("hotfix/security-patch")).not.toThrow();
|
|
});
|
|
|
|
it("should accept branch names with hyphens and underscores", () => {
|
|
expect(() => validateBranchName("feature-branch")).not.toThrow();
|
|
expect(() => validateBranchName("feature_branch")).not.toThrow();
|
|
expect(() => validateBranchName("feature-branch_v2")).not.toThrow();
|
|
});
|
|
|
|
it("should accept branch names with dots", () => {
|
|
expect(() => validateBranchName("release/1.0.0")).not.toThrow();
|
|
expect(() => validateBranchName("v2.5.1")).not.toThrow();
|
|
});
|
|
|
|
it("should accept branch names with numbers", () => {
|
|
expect(() => validateBranchName("feature-123")).not.toThrow();
|
|
expect(() => validateBranchName("123-bugfix")).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("Invalid branch names (Command Injection)", () => {
|
|
it("should reject empty or whitespace-only names", () => {
|
|
expect(() => validateBranchName("")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName(" ")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("\t")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject names starting with hyphen (option injection)", () => {
|
|
expect(() => validateBranchName("--config")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("-malicious")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject names with double dots (range specification)", () => {
|
|
expect(() => validateBranchName("feature..main")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("..malicious")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject names with path traversal patterns", () => {
|
|
expect(() => validateBranchName("../etc/passwd")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature/../main")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("malicious/..")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject names ending with .lock (reserved by git)", () => {
|
|
expect(() => validateBranchName("feature.lock")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("main.lock")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject names with special shell characters", () => {
|
|
expect(() => validateBranchName("feature;rm -rf /")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature$malicious")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature`whoami`")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature$(whoami)")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature|malicious")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature&malicious")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject names with control characters", () => {
|
|
expect(() => validateBranchName("feature\x00malicious")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature\x1Fmalicious")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature\x7Fmalicious")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject names exceeding maximum length", () => {
|
|
const longName = "a".repeat(256);
|
|
expect(() => validateBranchName(longName)).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject names with spaces", () => {
|
|
expect(() => validateBranchName("feature branch")).toThrow(BadRequestException);
|
|
expect(() => validateBranchName("feature branch")).toThrow(BadRequestException);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("validateRepositoryUrl", () => {
|
|
describe("Valid repository URLs", () => {
|
|
it("should accept HTTPS URLs", () => {
|
|
expect(() => validateRepositoryUrl("https://github.com/user/repo.git")).not.toThrow();
|
|
expect(() => validateRepositoryUrl("https://gitlab.com/group/project.git")).not.toThrow();
|
|
expect(() => validateRepositoryUrl("https://bitbucket.org/user/repo.git")).not.toThrow();
|
|
});
|
|
|
|
it("should accept HTTP URLs (for development)", () => {
|
|
expect(() => validateRepositoryUrl("http://git.example.com/repo.git")).not.toThrow();
|
|
});
|
|
|
|
it("should accept SSH URLs with git@ format", () => {
|
|
expect(() => validateRepositoryUrl("[email protected]:user/repo.git")).not.toThrow();
|
|
expect(() => validateRepositoryUrl("ssh://[email protected]/user/repo.git")).not.toThrow();
|
|
});
|
|
|
|
it("should accept git:// protocol", () => {
|
|
expect(() => validateRepositoryUrl("git://github.com/user/repo.git")).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("Invalid repository URLs (Security Risks)", () => {
|
|
it("should reject empty or whitespace-only URLs", () => {
|
|
expect(() => validateRepositoryUrl("")).toThrow(BadRequestException);
|
|
expect(() => validateRepositoryUrl(" ")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject dangerous protocols (file://)", () => {
|
|
expect(() => validateRepositoryUrl("file:///etc/passwd")).toThrow(BadRequestException);
|
|
expect(() => validateRepositoryUrl("file://C:/Windows/System32")).toThrow(
|
|
BadRequestException
|
|
);
|
|
});
|
|
|
|
it("should reject dangerous protocols (javascript:, data:)", () => {
|
|
expect(() => validateRepositoryUrl("javascript:alert('XSS')")).toThrow(BadRequestException);
|
|
expect(() => validateRepositoryUrl("data:text/html,<script>alert('XSS')</script>")).toThrow(
|
|
BadRequestException
|
|
);
|
|
});
|
|
|
|
it("should reject localhost URLs (SSRF protection)", () => {
|
|
expect(() => validateRepositoryUrl("https://localhost/repo.git")).toThrow(
|
|
BadRequestException
|
|
);
|
|
expect(() => validateRepositoryUrl("https://127.0.0.1/repo.git")).toThrow(
|
|
BadRequestException
|
|
);
|
|
expect(() => validateRepositoryUrl("https://0.0.0.0/repo.git")).toThrow(BadRequestException);
|
|
expect(() => validateRepositoryUrl("http://::1/repo.git")).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject internal network URLs (SSRF protection)", () => {
|
|
expect(() => validateRepositoryUrl("https://192.168.1.1/repo.git")).toThrow(
|
|
BadRequestException
|
|
);
|
|
expect(() => validateRepositoryUrl("https://10.0.0.1/repo.git")).toThrow(BadRequestException);
|
|
expect(() => validateRepositoryUrl("https://172.16.0.1/repo.git")).toThrow(
|
|
BadRequestException
|
|
);
|
|
});
|
|
|
|
it("should reject URLs with embedded credentials", () => {
|
|
expect(() => validateRepositoryUrl("https://user:[email protected]/repo.git")).toThrow(
|
|
BadRequestException
|
|
);
|
|
});
|
|
|
|
it("should reject URLs with shell special characters", () => {
|
|
expect(() => validateRepositoryUrl("https://github.com/repo.git;whoami")).toThrow(
|
|
BadRequestException
|
|
);
|
|
expect(() => validateRepositoryUrl("https://github.com/repo.git|malicious")).toThrow(
|
|
BadRequestException
|
|
);
|
|
expect(() => validateRepositoryUrl("https://github.com/repo.git&malicious")).toThrow(
|
|
BadRequestException
|
|
);
|
|
expect(() => validateRepositoryUrl("https://github.com/repo.git$malicious")).toThrow(
|
|
BadRequestException
|
|
);
|
|
expect(() => validateRepositoryUrl("https://github.com/repo.git`whoami`")).toThrow(
|
|
BadRequestException
|
|
);
|
|
});
|
|
|
|
it("should reject URLs exceeding maximum length", () => {
|
|
const longUrl = "https://github.com/" + "a".repeat(2000) + ".git";
|
|
expect(() => validateRepositoryUrl(longUrl)).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject unknown/dangerous protocols", () => {
|
|
expect(() => validateRepositoryUrl("ftp://example.com/repo.git")).toThrow(
|
|
BadRequestException
|
|
);
|
|
expect(() => validateRepositoryUrl("telnet://example.com")).toThrow(BadRequestException);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("validateSpawnContext", () => {
|
|
it("should validate both repository and branch", () => {
|
|
expect(() =>
|
|
validateSpawnContext({
|
|
repository: "https://github.com/user/repo.git",
|
|
branch: "feature/add-login",
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("should reject invalid repository", () => {
|
|
expect(() =>
|
|
validateSpawnContext({
|
|
repository: "file:///etc/passwd",
|
|
branch: "main",
|
|
})
|
|
).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject invalid branch", () => {
|
|
expect(() =>
|
|
validateSpawnContext({
|
|
repository: "https://github.com/user/repo.git",
|
|
branch: "--config malicious",
|
|
})
|
|
).toThrow(BadRequestException);
|
|
});
|
|
|
|
it("should reject both invalid repository and branch", () => {
|
|
expect(() =>
|
|
validateSpawnContext({
|
|
repository: "javascript:alert('XSS')",
|
|
branch: "$(whoami)",
|
|
})
|
|
).toThrow(BadRequestException);
|
|
});
|
|
});
|