Skip to content

Commit 80453c7

Browse files
committed
Address thresher.sh findings, introduce trust flow for injectable code, remove path traversal vuln, harden workflows for supplychain attacks
1 parent c765e62 commit 80453c7

File tree

12 files changed

+231
-33
lines changed

12 files changed

+231
-33
lines changed

.github/workflows/homebrew.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ on:
44
release:
55
types: [published]
66

7+
permissions: read-all
8+
79
jobs:
810
homebrew:
911
name: Bump Homebrew formula
1012
runs-on: ubuntu-latest
1113
if: ${{ !github.event.release.prerelease }}
1214
steps:
13-
- uses: mislav/bump-homebrew-formula-action@v3
15+
- uses: mislav/bump-homebrew-formula-action@56a283fa15557e9abaa4bdb63b8212abc68e655c # v3
1416
with:
1517
formula-name: git-gtr
1618
formula-path: Formula/git-gtr.rb

.github/workflows/lint.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ on:
66
pull_request:
77
branches: [main]
88

9+
permissions: read-all
10+
911
jobs:
1012
shellcheck:
1113
name: ShellCheck
1214
runs-on: ubuntu-latest
1315
steps:
14-
- uses: actions/checkout@v4
16+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1517

1618
- name: Install ShellCheck
1719
run: sudo apt-get update && sudo apt-get install -y shellcheck
@@ -24,7 +26,7 @@ jobs:
2426
name: Completions
2527
runs-on: ubuntu-latest
2628
steps:
27-
- uses: actions/checkout@v4
29+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
2830

2931
- name: Verify completion files are up to date
3032
run: ./scripts/generate-completions.sh --check
@@ -33,7 +35,7 @@ jobs:
3335
name: Tests
3436
runs-on: ubuntu-latest
3537
steps:
36-
- uses: actions/checkout@v4
38+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
3739

3840
- name: Install BATS
3941
run: sudo apt-get update && sudo apt-get install -y bats

bin/git-gtr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ main() {
100100
init)
101101
cmd_init "$@"
102102
;;
103+
trust)
104+
cmd_trust "$@"
105+
;;
103106
version|--version|-v)
104107
echo "git gtr version $GTR_VERSION"
105108
;;

lib/adapters.sh

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,10 @@ editor_open() {
166166
target="$workspace"
167167
fi
168168

169-
# $GTR_EDITOR_CMD may contain arguments (e.g., "code --wait")
170-
# Using eval here is necessary to handle multi-word commands properly
171-
eval "$GTR_EDITOR_CMD \"\$target\""
169+
# Split multi-word commands (e.g., "code --wait") into an array for safe execution
170+
local _cmd_arr
171+
read -ra _cmd_arr <<< "$GTR_EDITOR_CMD"
172+
"${_cmd_arr[@]}" "$target"
172173
}
173174

174175
# Globals set by load_ai_adapter: GTR_AI_CMD, GTR_AI_CMD_NAME
@@ -179,9 +180,10 @@ ai_can_start() {
179180
ai_start() {
180181
local path="$1"
181182
shift
182-
# $GTR_AI_CMD may contain arguments (e.g., "bunx @github/copilot@latest")
183-
# Using eval here is necessary to handle multi-word commands properly
184-
(cd "$path" && eval "$GTR_AI_CMD \"\$@\"")
183+
# Split multi-word commands (e.g., "bunx @github/copilot@latest") into an array for safe execution
184+
local _cmd_arr
185+
read -ra _cmd_arr <<< "$GTR_AI_CMD"
186+
(cd "$path" && "${_cmd_arr[@]}" "$@")
185187
}
186188

187189
# Standard AI adapter builder — used by adapter files that follow the common pattern
@@ -295,6 +297,15 @@ resolve_workspace_file() {
295297
# Usage: _load_adapter <type> <name> <label> <builtin_list> <path_hint>
296298
_load_adapter() {
297299
local type="$1" name="$2" label="$3" builtin_list="$4" path_hint="$5"
300+
301+
# Reject adapter names containing path traversal characters
302+
case "$name" in
303+
*/* | *..* | *\\*)
304+
log_error "$label name '$name' contains invalid characters"
305+
return 1
306+
;;
307+
esac
308+
298309
local adapter_file="$GTR_DIR/adapters/${type}/${name}.sh"
299310

300311
# 1. Try loading explicit adapter file (custom overrides like claude, nano)
@@ -332,6 +343,17 @@ _load_adapter() {
332343
return 1
333344
fi
334345

346+
# Reject shell metacharacters in config-supplied command names to prevent injection
347+
# Allows multi-word commands (e.g., "code --wait") but blocks shell operators
348+
# shellcheck disable=SC2016 # Literal '$(' pattern match is intentional
349+
case "$name" in
350+
*\;* | *\`* | *'$('* | *\|* | *\&* | *'>'* | *'<'*)
351+
log_error "$label '$name' contains shell metacharacters — refusing to execute"
352+
log_info "Use a simple command name, optionally with flags (e.g., 'code --wait')"
353+
return 1
354+
;;
355+
esac
356+
335357
# Set globals for generic adapter functions
336358
# Note: $name may contain arguments (e.g., "code --wait", "bunx @github/copilot@latest")
337359
if [ "$type" = "editor" ]; then

lib/args.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,13 @@ _pa_match_flag() {
3939

4040
# Check if $flag matches any alternative in the pattern
4141
local alt matched=0
42-
local IFS_save="$IFS"
43-
IFS="|"
42+
local IFS="|"
4443
for alt in $line; do
4544
if [ "$flag" = "$alt" ]; then
4645
matched=1
4746
break
4847
fi
4948
done
50-
IFS="$IFS_save"
5149

5250
if [ "$matched" = "1" ]; then
5351
# Derive variable name from the first (canonical) pattern

lib/commands/init.sh

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,23 +96,31 @@ _init_bash() {
9696
9797
__FUNC___run_post_cd_hooks() {
9898
local dir="$1"
99+
local _gtr_trust_dir="${XDG_CONFIG_HOME:-$HOME/.config}/gtr/trusted"
99100
100101
cd "$dir" && {
101102
local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file
102103
_gtr_hooks=""
103104
_gtr_seen=""
104105
# Read from git config (local > global > system)
105106
_gtr_hooks="$(git config --get-all gtr.hook.postCd 2>/dev/null)" || true
106-
# Read from .gtrconfig if it exists
107+
# Read from .gtrconfig if it exists — only if trusted
107108
_gtr_config_file="$(git rev-parse --show-toplevel 2>/dev/null)/.gtrconfig"
108109
if [ -f "$_gtr_config_file" ]; then
109110
local _gtr_file_hooks
110111
_gtr_file_hooks="$(git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)" || true
111112
if [ -n "$_gtr_file_hooks" ]; then
112-
if [ -n "$_gtr_hooks" ]; then
113-
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
113+
# Verify trust before including .gtrconfig hooks
114+
local _gtr_hook_hash
115+
_gtr_hook_hash="$(git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null | shasum -a 256 | cut -d' ' -f1)" || true
116+
if [ -n "$_gtr_hook_hash" ] && [ -f "$_gtr_trust_dir/$_gtr_hook_hash" ]; then
117+
if [ -n "$_gtr_hooks" ]; then
118+
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
119+
else
120+
_gtr_hooks="$_gtr_file_hooks"
121+
fi
114122
else
115-
_gtr_hooks="$_gtr_file_hooks"
123+
echo "__FUNC__: Untrusted .gtrconfig hooks skipped — run 'git gtr trust' to approve" >&2
116124
fi
117125
fi
118126
fi
@@ -273,23 +281,31 @@ _init_zsh() {
273281
__FUNC___run_post_cd_hooks() {
274282
emulate -L zsh
275283
local dir="$1"
284+
local _gtr_trust_dir="${XDG_CONFIG_HOME:-$HOME/.config}/gtr/trusted"
276285
277286
cd "$dir" && {
278287
local _gtr_hooks _gtr_hook _gtr_seen _gtr_config_file
279288
_gtr_hooks=""
280289
_gtr_seen=""
281290
# Read from git config (local > global > system)
282291
_gtr_hooks="$(git config --get-all gtr.hook.postCd 2>/dev/null)" || true
283-
# Read from .gtrconfig if it exists
292+
# Read from .gtrconfig if it exists — only if trusted
284293
_gtr_config_file="$(git rev-parse --show-toplevel 2>/dev/null)/.gtrconfig"
285294
if [ -f "$_gtr_config_file" ]; then
286295
local _gtr_file_hooks
287296
_gtr_file_hooks="$(git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)" || true
288297
if [ -n "$_gtr_file_hooks" ]; then
289-
if [ -n "$_gtr_hooks" ]; then
290-
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
298+
# Verify trust before including .gtrconfig hooks
299+
local _gtr_hook_hash
300+
_gtr_hook_hash="$(git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null | shasum -a 256 | cut -d' ' -f1)" || true
301+
if [ -n "$_gtr_hook_hash" ] && [ -f "$_gtr_trust_dir/$_gtr_hook_hash" ]; then
302+
if [ -n "$_gtr_hooks" ]; then
303+
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
304+
else
305+
_gtr_hooks="$_gtr_file_hooks"
306+
fi
291307
else
292-
_gtr_hooks="$_gtr_file_hooks"
308+
echo "__FUNC__: Untrusted .gtrconfig hooks skipped — run 'git gtr trust' to approve" >&2
293309
fi
294310
fi
295311
fi
@@ -451,17 +467,30 @@ _init_fish() {
451467
452468
function __FUNC___run_post_cd_hooks
453469
set -l dir "$argv[1]"
470+
set -l _gtr_trust_dir "$HOME/.config/gtr/trusted"
471+
if set -q XDG_CONFIG_HOME
472+
set _gtr_trust_dir "$XDG_CONFIG_HOME/gtr/trusted"
473+
end
454474
cd $dir
455475
and begin
456476
set -l _gtr_hooks
457477
set -l _gtr_seen
458478
# Read from git config (local > global > system)
459479
set -l _gtr_git_hooks (git config --get-all gtr.hook.postCd 2>/dev/null)
460-
# Read from .gtrconfig if it exists
480+
# Read from .gtrconfig if it exists — only if trusted
461481
set -l _gtr_config_file (git rev-parse --show-toplevel 2>/dev/null)"/.gtrconfig"
462482
set -l _gtr_file_hooks
463483
if test -f "$_gtr_config_file"
464-
set _gtr_file_hooks (git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)
484+
set -l _gtr_candidate_hooks (git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)
485+
if test (count $_gtr_candidate_hooks) -gt 0
486+
# Verify trust before including .gtrconfig hooks
487+
set -l _gtr_hook_hash (git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
488+
if test -n "$_gtr_hook_hash"; and test -f "$_gtr_trust_dir/$_gtr_hook_hash"
489+
set _gtr_file_hooks $_gtr_candidate_hooks
490+
else
491+
echo "__FUNC__: Untrusted .gtrconfig hooks skipped — run 'git gtr trust' to approve" >&2
492+
end
493+
end
465494
end
466495
# Merge and deduplicate
467496
set _gtr_hooks $_gtr_git_hooks $_gtr_file_hooks

lib/commands/trust.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
# Trust management for .gtrconfig hooks
3+
4+
cmd_trust() {
5+
local config_file
6+
config_file=$(_gtrconfig_path)
7+
8+
if [ -z "$config_file" ] || [ ! -f "$config_file" ]; then
9+
log_info "No .gtrconfig file found in this repository"
10+
return 0
11+
fi
12+
13+
# Show all hook entries from .gtrconfig
14+
local hook_content
15+
hook_content=$(git config -f "$config_file" --get-regexp '^hooks\.' 2>/dev/null) || true
16+
17+
if [ -z "$hook_content" ]; then
18+
log_info "No hooks defined in $config_file"
19+
return 0
20+
fi
21+
22+
if _hooks_are_trusted "$config_file"; then
23+
log_info "Hooks in $config_file are already trusted"
24+
log_info "Current hooks:"
25+
printf '%s\n' "$hook_content" >&2
26+
return 0
27+
fi
28+
29+
log_warn "The following hooks are defined in $config_file:"
30+
echo "" >&2
31+
printf '%s\n' "$hook_content" >&2
32+
echo "" >&2
33+
log_warn "These commands will execute on your machine during gtr operations."
34+
35+
if prompt_yes_no "Trust these hooks?"; then
36+
_hooks_mark_trusted "$config_file"
37+
log_info "Hooks marked as trusted"
38+
else
39+
log_info "Hooks remain untrusted and will not execute"
40+
fi
41+
}

lib/config.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,9 @@ cfg_default() {
408408
value=$(git config --get "$key" 2>/dev/null || true)
409409
fi
410410

411-
# 4. Fall back to environment variable (POSIX-compliant indirect reference)
411+
# 4. Fall back to environment variable
412412
if [ -z "$value" ] && [ -n "$env_name" ]; then
413-
eval "value=\${${env_name}:-}"
413+
value=$(printenv "$env_name" 2>/dev/null) || true
414414
fi
415415

416416
# 5. Use fallback if still empty

lib/copy.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,14 @@ _apply_directory_excludes() {
282282
local pattern_prefix="${exclude_pattern%%/*}"
283283
local pattern_suffix="${exclude_pattern#*/}"
284284

285+
# Reject bare glob-only suffixes that would match everything
286+
case "$pattern_suffix" in
287+
""|"*"|"**"|".*")
288+
log_warn "Skipping overly broad exclude suffix: $exclude_pattern"
289+
continue
290+
;;
291+
esac
292+
285293
# Intentional glob pattern matching for directory prefix
286294
# shellcheck disable=SC2254
287295
case "$dir_path" in
@@ -295,8 +303,13 @@ _apply_directory_excludes() {
295303
shopt -s dotglob 2>/dev/null || true
296304

297305
local removed_any=0
306+
# shellcheck disable=SC2086
298307
for matched_path in $pattern_suffix; do
299308
if [ -e "$matched_path" ]; then
309+
# Never remove .git directory via exclude patterns
310+
case "$matched_path" in
311+
.git|.git/*) continue ;;
312+
esac
300313
if rm -rf "$matched_path" 2>/dev/null; then
301314
removed_any=1
302315
fi

0 commit comments

Comments
 (0)