From 3030c627b44a3a2e512c68150bb6c4d4f376c7eb Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Wed, 4 Feb 2026 09:01:04 -0800 Subject: [PATCH] Add branch name validation to prevent path traversal and conflicts Signed-off-by: Cong Wang --- src/branch.rs | 30 +++++++++++ tests/test_branch_validate.sh | 94 +++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 tests/test_branch_validate.sh diff --git a/src/branch.rs b/src/branch.rs index 18ad939..9f2c9a0 100644 --- a/src/branch.rs +++ b/src/branch.rs @@ -87,6 +87,34 @@ impl Branch { } } +fn validate_branch_name(name: &str) -> Result<()> { + if name.is_empty() { + return Err(BranchError::Invalid("branch name cannot be empty".into())); + } + if name == "." || name == ".." { + return Err(BranchError::Invalid(format!( + "'{}' is not a valid branch name", + name + ))); + } + if name.contains('/') || name.contains('\0') { + return Err(BranchError::Invalid( + "branch name cannot contain '/' or null bytes".into(), + )); + } + if name.starts_with('@') { + return Err(BranchError::Invalid( + "branch name cannot start with '@' (reserved for virtual paths)".into(), + )); + } + if name.len() > 255 { + return Err(BranchError::Invalid( + "branch name cannot exceed 255 characters".into(), + )); + } + Ok(()) +} + pub struct BranchManager { pub storage_path: PathBuf, pub base_path: PathBuf, @@ -122,6 +150,8 @@ impl BranchManager { } pub fn create_branch(&self, name: &str, parent: &str) -> Result<()> { + validate_branch_name(name)?; + let mut branches = self.branches.write(); if branches.contains_key(name) { diff --git a/tests/test_branch_validate.sh b/tests/test_branch_validate.sh new file mode 100644 index 0000000..f96f82c --- /dev/null +++ b/tests/test_branch_validate.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Test branch name validation + +source "$(dirname "$0")/test_helper.sh" + +# Helper: assert that branch creation fails with a specific error substring +assert_create_fails() { + local name="$1" + local expected_err="$2" + local message="$3" + + local output + output=$("$BRANCHFS" create "$name" "$TEST_MNT" -p main --storage "$TEST_STORAGE" 2>&1) && { + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) + echo -e " ${RED}✗${NC} $message" + echo -e " ${RED}Expected failure but command succeeded${NC}" + return 1 + } + + TESTS_RUN=$((TESTS_RUN + 1)) + if [[ "$output" == *"$expected_err"* ]]; then + TESTS_PASSED=$((TESTS_PASSED + 1)) + echo -e " ${GREEN}✓${NC} $message" + return 0 + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + echo -e " ${RED}✗${NC} $message" + echo -e " ${RED}Expected error containing: $expected_err${NC}" + echo -e " ${RED}Actual output: $output${NC}" + return 1 + fi +} + +test_reject_empty_name() { + setup + do_mount + + assert_create_fails "" "empty" "Rejects empty branch name" + + do_unmount +} + +test_reject_dot_names() { + setup + do_mount + + assert_create_fails ".." "not a valid branch name" "Rejects '..' as branch name" + + do_unmount +} + +test_reject_slash_in_name() { + setup + do_mount + + assert_create_fails "foo/bar" "cannot contain '/'" "Rejects name with '/'" + + do_unmount +} + +test_reject_at_prefix() { + setup + do_mount + + assert_create_fails "@mybranch" "cannot start with '@'" "Rejects name starting with '@'" + + do_unmount +} + +test_valid_names_work() { + setup + do_mount + + do_create "feature-1" + assert_branch_exists "feature-1" "Hyphenated name works" + + do_create "my_branch" + assert_branch_exists "my_branch" "Underscored name works" + + do_create "release42" + assert_branch_exists "release42" "Alphanumeric name works" + + do_unmount +} + +# Run tests +run_test "Reject Empty Name" test_reject_empty_name +run_test "Reject Dot Names" test_reject_dot_names +run_test "Reject Slash in Name" test_reject_slash_in_name +run_test "Reject @ Prefix" test_reject_at_prefix +run_test "Valid Names Work" test_valid_names_work + +print_summary