Skip to content
Merged
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
4 changes: 3 additions & 1 deletion lib/config_safety.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ _config_safety_path() {
if [ -L "$candidate" ]; then
links=$((links + 1))
[ "$links" -le 40 ] || { _config_safety_error "$candidate" 'symlink loop'; return 1; }
target=$(readlink "$candidate") || return 1
# Suppress readlink's delimiter and keep target newlines with a sentinel.
target=$(readlink -n "$candidate" && printf '.') || return 1
target="${target%.}"
case "$target" in /*) current=/ ;; esac
pending="$target${pending:+/$pending}"
else
Expand Down
56 changes: 56 additions & 0 deletions tests/config_safety.bats
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,62 @@ assert_unsafe_shell_blocked() {
assert_unsafe_create_blocked
}

@test "unsafe absolute symlink target with trailing newlines is rejected" {
local target="$SAFETY_FIXTURE/included"$'\n\n'
git config -f "$target" gtr.hook.postCreate 'printf linked > "$SAFETY_MARKER"'
chmod 0660 "$target"
ln -s "$target" "$TEST_REPO/.git/linked-config"
git config include.path linked-config

assert_unsafe_create_blocked
[[ "$output" == *"$target"* ]]
}

@test "unsafe relative symlink target with a trailing newline is rejected" {
local target="$TEST_REPO/.git/included"$'\n'
git config -f "$target" gtr.hook.postCreate 'printf linked > "$SAFETY_MARKER"'
chmod 0660 "$target"
ln -s $'included\n' "$TEST_REPO/.git/linked-config"
git config include.path linked-config

assert_unsafe_create_blocked
[[ "$output" == *"$target"* ]]
}

@test "safe symlink target with trailing newlines is used instead of its writable truncated path" {
local target="$SAFETY_FIXTURE/included"$'\n\n'
git config -f "$target" gtr.hook.postCreate 'printf linked > "$SAFETY_MARKER"'
chmod 0600 "$target"
: > "$SAFETY_FIXTURE/included"
chmod 0660 "$SAFETY_FIXTURE/included"
ln -s "$target" "$TEST_REPO/.git/linked-config"
git config include.path linked-config

run "$PROJECT_ROOT/bin/gtr" new safe-newline --from HEAD --no-fetch --no-copy
[ "$status" -eq 0 ]
[ -d "$SAFETY_FIXTURE/attacker destination/safe-newline" ]
[ "$(cat "$SAFETY_MARKER")" = linked ]
}

@test "readlink failure rejects config even when it prints a safe target" {
git config -f "$SAFETY_FIXTURE/protected" gtr.hook.postCreate 'printf linked > "$SAFETY_MARKER"'
ln -s "$SAFETY_FIXTURE/protected" "$TEST_REPO/.git/linked-config"
git config include.path linked-config
readlink() {
printf '%s\n' "$SAFETY_FIXTURE/protected"
return 1
}
export SAFETY_FIXTURE
export -f readlink

run "$PROJECT_ROOT/bin/gtr" new unsafe-config --from HEAD --no-fetch --no-copy
[ "$status" -ne 0 ]
[ ! -e "$SAFETY_MARKER" ]
[ ! -e "$SAFETY_FIXTURE/attacker destination" ]
run git show-ref --verify --quiet refs/heads/unsafe-config
[ "$status" -eq 1 ]
}

@test "symlink parent permissions are checked before following a safe target" {
mkdir "$SAFETY_FIXTURE/shared"
git config -f "$SAFETY_FIXTURE/protected" gtr.hook.postCreate 'printf linked > "$SAFETY_MARKER"'
Expand Down
Loading