Skip to content

Commit 825820e

Browse files
committed
fix: harden adapter execution and trust markers
1 parent f2b7249 commit 825820e

File tree

9 files changed

+311
-44
lines changed

9 files changed

+311
-44
lines changed

adapters/ai/claude.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ ai_can_start() {
4242
ai_start() {
4343
local path="$1"
4444
shift
45+
local configured_args=("${GTR_AI_CMD_ARGS[@]}")
4546

4647
local claude_cmd
4748
claude_cmd="$(find_claude_executable)"
@@ -57,5 +58,5 @@ ai_start() {
5758
return 1
5859
fi
5960

60-
(cd "$path" && "$claude_cmd" "$@")
61+
(cd "$path" && "$claude_cmd" "${configured_args[@]}" "$@")
6162
}

adapters/ai/cursor.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ai_can_start() {
1111
ai_start() {
1212
local path="$1"
1313
shift
14+
local configured_args=("${GTR_AI_CMD_ARGS[@]}")
1415

1516
if ! ai_can_start; then
1617
log_error "Cursor not found. Install from https://cursor.com"
@@ -25,9 +26,9 @@ ai_start() {
2526

2627
# Try cursor-agent first, then fallback to cursor CLI commands
2728
if command -v cursor-agent >/dev/null 2>&1; then
28-
(cd "$path" && cursor-agent "$@")
29+
(cd "$path" && cursor-agent "${configured_args[@]}" "$@")
2930
elif command -v cursor >/dev/null 2>&1; then
3031
# Try various Cursor CLI patterns (implementation varies by version)
31-
(cd "$path" && cursor cli "$@") 2>/dev/null || (cd "$path" && cursor "$@")
32+
(cd "$path" && cursor cli "${configured_args[@]}" "$@") 2>/dev/null || (cd "$path" && cursor "${configured_args[@]}" "$@")
3233
fi
3334
}

adapters/editor/nano.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ editor_can_open() {
1010
# Usage: editor_open path
1111
editor_open() {
1212
local path="$1"
13+
local configured_args=("${GTR_EDITOR_CMD_ARGS[@]}")
1314

1415
if ! editor_can_open; then
1516
log_error "Nano not found. Usually pre-installed on Unix systems."
1617
return 1
1718
fi
1819

20+
if [ "${#configured_args[@]}" -gt 0 ]; then
21+
(cd "$path" && nano "${configured_args[@]}")
22+
return $?
23+
fi
24+
1925
# Open nano in the directory (just cd there, nano doesn't open directories)
2026
log_info "Opening shell in $path (nano doesn't support directory mode)"
2127
(cd "$path" && exec "$SHELL")

lib/adapters.sh

Lines changed: 175 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,161 @@ ai_start() {
180180
(cd "$path" && _run_configured_command "$GTR_AI_CMD" "$@")
181181
}
182182

183+
# Split a config-supplied command string without shell evaluation.
184+
# Populates the global _GTR_PARSED_CMD_ARGS array.
185+
_parse_configured_command() {
186+
local command_string="$1"
187+
local length="${#command_string}"
188+
local i=0 char="" token="" state="normal" escaped=0 token_started=0
189+
190+
_GTR_PARSED_CMD_ARGS=()
191+
192+
while [ "$i" -lt "$length" ]; do
193+
char="${command_string:$i:1}"
194+
195+
case "$state" in
196+
normal)
197+
if [ "$escaped" -eq 1 ]; then
198+
token="${token}${char}"
199+
token_started=1
200+
escaped=0
201+
else
202+
case "$char" in
203+
"\\")
204+
escaped=1
205+
token_started=1
206+
;;
207+
" " | $'\t' | $'\n')
208+
if [ "$token_started" -eq 1 ]; then
209+
_GTR_PARSED_CMD_ARGS+=("$token")
210+
token=""
211+
token_started=0
212+
fi
213+
;;
214+
"'")
215+
state="single"
216+
token_started=1
217+
;;
218+
'"')
219+
state="double"
220+
token_started=1
221+
;;
222+
*)
223+
token="${token}${char}"
224+
token_started=1
225+
;;
226+
esac
227+
fi
228+
;;
229+
single)
230+
if [ "$char" = "'" ]; then
231+
state="normal"
232+
else
233+
token="${token}${char}"
234+
fi
235+
;;
236+
double)
237+
if [ "$escaped" -eq 1 ]; then
238+
token="${token}${char}"
239+
token_started=1
240+
escaped=0
241+
else
242+
case "$char" in
243+
"\\")
244+
escaped=1
245+
token_started=1
246+
;;
247+
'"')
248+
state="normal"
249+
;;
250+
*)
251+
token="${token}${char}"
252+
token_started=1
253+
;;
254+
esac
255+
fi
256+
;;
257+
esac
258+
259+
i=$((i + 1))
260+
done
261+
262+
[ "$escaped" -eq 0 ] || return 1
263+
[ "$state" = "normal" ] || return 1
264+
265+
if [ "$token_started" -eq 1 ]; then
266+
_GTR_PARSED_CMD_ARGS+=("$token")
267+
fi
268+
269+
[ "${#_GTR_PARSED_CMD_ARGS[@]}" -gt 0 ]
270+
}
271+
272+
_configured_command_uses_path_arg() {
273+
local arg="$1"
274+
case "$arg" in
275+
/* | ./* | ../* | ~* | *\\*)
276+
return 0
277+
;;
278+
esac
279+
return 1
280+
}
281+
282+
_configured_command_is_wrapper() {
283+
local cmd_name="$1"
284+
case "$cmd_name" in
285+
sh | bash | zsh | dash | ksh | fish | env | eval | source | . | python | python3 | node | ruby | perl | php | lua | pwsh | powershell)
286+
return 0
287+
;;
288+
esac
289+
return 1
290+
}
291+
292+
_validate_configured_command() {
293+
local command_string="$1"
294+
295+
# Reject shell metacharacters in config-supplied command names to prevent injection.
296+
# shellcheck disable=SC2016 # Literal '$(' pattern match is intentional
297+
case "$command_string" in
298+
*\;* | *\`* | *'$('* | *\|* | *\&* | *'>'* | *'<'*)
299+
return 1
300+
;;
301+
esac
302+
303+
_parse_configured_command "$command_string" || return 1
304+
[ "${#_GTR_PARSED_CMD_ARGS[@]}" -gt 0 ] || return 1
305+
306+
local cmd_name="${_GTR_PARSED_CMD_ARGS[0]}"
307+
308+
case "$cmd_name" in
309+
*/* | *\\*)
310+
return 1
311+
;;
312+
esac
313+
314+
if _configured_command_is_wrapper "$cmd_name" && [ "${#_GTR_PARSED_CMD_ARGS[@]}" -gt 1 ]; then
315+
return 1
316+
fi
317+
318+
local arg
319+
for arg in "${_GTR_PARSED_CMD_ARGS[@]:1}"; do
320+
if _configured_command_uses_path_arg "$arg"; then
321+
return 1
322+
fi
323+
done
324+
325+
type -P "$cmd_name" >/dev/null 2>&1
326+
}
327+
183328
# Parse and run a config-supplied command string while preserving quoted args.
184329
_run_configured_command() {
185330
local command_string="$1"
186331
shift
187332
local extra_args=("$@")
188333

189-
(
190-
eval "set -- $command_string" || exit 1
191-
[ "$#" -gt 0 ] || exit 1
192-
"$@" "${extra_args[@]}"
193-
)
334+
_parse_configured_command "$command_string" || return 1
335+
[ "${#_GTR_PARSED_CMD_ARGS[@]}" -gt 0 ] || return 1
336+
337+
"${_GTR_PARSED_CMD_ARGS[@]}" "${extra_args[@]}"
194338
}
195339

196340
# Standard AI adapter builder — used by adapter files that follow the common pattern
@@ -304,7 +448,14 @@ resolve_workspace_file() {
304448
# Usage: _load_adapter <type> <name> <label> <builtin_list> <path_hint>
305449
_load_adapter() {
306450
local type="$1" name="$2" label="$3" builtin_list="$4" path_hint="$5"
307-
local adapter_selector="${name%% *}"
451+
if ! _validate_configured_command "$name"; then
452+
log_error "$label '$name' is not a safe executable command"
453+
log_info "Use a PATH command name, optionally with flags (e.g., 'code --wait')"
454+
return 1
455+
fi
456+
457+
local adapter_selector="${_GTR_PARSED_CMD_ARGS[0]}"
458+
local cmd_args=("${_GTR_PARSED_CMD_ARGS[@]:1}")
308459

309460
local adapter_file="$GTR_DIR/adapters/${type}/${adapter_selector}.sh"
310461

@@ -313,6 +464,17 @@ _load_adapter() {
313464
*/* | *..* | *\\*) ;;
314465
*)
315466
if [ -f "$adapter_file" ]; then
467+
if [ "$type" = "editor" ]; then
468+
# shellcheck disable=SC2034 # Used by sourced override adapters.
469+
GTR_EDITOR_CMD="$name"
470+
GTR_EDITOR_CMD_NAME="$adapter_selector"
471+
GTR_EDITOR_CMD_ARGS=("${cmd_args[@]}")
472+
else
473+
# shellcheck disable=SC2034 # Used by sourced override adapters.
474+
GTR_AI_CMD="$name"
475+
GTR_AI_CMD_NAME="$adapter_selector"
476+
GTR_AI_CMD_ARGS=("${cmd_args[@]}")
477+
fi
316478
# shellcheck disable=SC1090
317479
. "$adapter_file"
318480
return 0
@@ -337,44 +499,27 @@ _load_adapter() {
337499
return 0
338500
fi
339501

340-
# 3. Generic fallback: check if command exists in PATH
341-
# Extract first word (command name) from potentially multi-word string
342-
local cmd_name="${name%% *}"
343-
344-
case "$cmd_name" in
345-
*/* | *\\*)
346-
log_error "$label '$name' must use a PATH command name, not a filesystem path"
347-
log_info "Use a simple command name, optionally with flags (e.g., 'code --wait')"
348-
return 1
349-
;;
350-
esac
351-
352-
if ! command -v "$cmd_name" >/dev/null 2>&1; then
502+
# 3. Generic fallback: command already validated and resolved in PATH
503+
local cmd_name="$adapter_selector"
504+
if ! type -P "$cmd_name" >/dev/null 2>&1; then
353505
log_error "$label '$name' not found"
354506
log_info "Built-in adapters: $builtin_list"
355507
log_info "Or use any $label command available in your PATH (e.g., $path_hint)"
356508
return 1
357509
fi
358510

359-
# Reject shell metacharacters in config-supplied command names to prevent injection
360-
# Allows multi-word commands (e.g., "code --wait") but blocks shell operators
361-
# shellcheck disable=SC2016 # Literal '$(' pattern match is intentional
362-
case "$name" in
363-
*\;* | *\`* | *'$('* | *\|* | *\&* | *'>'* | *'<'*)
364-
log_error "$label '$name' contains shell metacharacters — refusing to execute"
365-
log_info "Use a simple command name, optionally with flags (e.g., 'code --wait')"
366-
return 1
367-
;;
368-
esac
369-
370511
# Set globals for generic adapter functions
371512
# Note: $name may contain arguments (e.g., "code --wait", "bunx @github/copilot@latest")
372513
if [ "$type" = "editor" ]; then
373514
GTR_EDITOR_CMD="$name"
374515
GTR_EDITOR_CMD_NAME="$cmd_name"
516+
# shellcheck disable=SC2034 # Used by sourced override adapters.
517+
GTR_EDITOR_CMD_ARGS=("${cmd_args[@]}")
375518
else
376519
GTR_AI_CMD="$name"
377520
GTR_AI_CMD_NAME="$cmd_name"
521+
# shellcheck disable=SC2034 # Used by sourced override adapters.
522+
GTR_AI_CMD_ARGS=("${cmd_args[@]}")
378523
fi
379524
}
380525

0 commit comments

Comments
 (0)