Skip to content

Commit beef04a

Browse files
fix(security): harden credential preflight PATH
1 parent 910857f commit beef04a

2 files changed

Lines changed: 71 additions & 12 deletions

File tree

scripts/lib/credential_preflight.sh

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
set -euo pipefail
1111

12+
CRED_PREFLIGHT_SYSTEM_PATH="/usr/bin:/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/sbin"
13+
PATH="$CRED_PREFLIGHT_SYSTEM_PATH"
14+
export PATH
15+
1216
CRED_PREFLIGHT_JSON=false
1317
CRED_PREFLIGHT_ROOT=""
1418
CRED_PREFLIGHT_HOME="${HOME:-}"
@@ -23,6 +27,7 @@ CRED_PREFLIGHT_EXCLUDES=()
2327
CRED_PREFLIGHT_FINDINGS=()
2428
CRED_PREFLIGHT_SKIPPED=()
2529
CRED_PREFLIGHT_FILES_SCANNED=0
30+
CRED_PREFLIGHT_JQ_BIN=""
2631

2732
credential_preflight_usage() {
2833
cat <<'EOF'
@@ -102,25 +107,49 @@ credential_preflight_parse_args() {
102107

103108
credential_preflight_binary_path() {
104109
local name="${1:-}"
105-
local path_value=""
110+
local candidate=""
106111

107112
[[ -n "$name" ]] || return 1
108113
case "$name" in
109-
.|..|*/*) return 1 ;;
114+
.|..|*/*|*[!A-Za-z0-9._+-]*) return 1 ;;
110115
esac
111116

112-
path_value="$(command -v "$name" 2>/dev/null || true)"
113-
[[ -n "$path_value" && -x "$path_value" ]] || return 1
114-
printf '%s\n' "$path_value"
117+
for candidate in \
118+
"/usr/bin/$name" \
119+
"/bin/$name" \
120+
"/usr/local/bin/$name" \
121+
"/usr/local/sbin/$name" \
122+
"/usr/sbin/$name" \
123+
"/sbin/$name"
124+
do
125+
[[ -x "$candidate" ]] || continue
126+
printf '%s\n' "$candidate"
127+
return 0
128+
done
129+
130+
return 1
115131
}
116132

117133
credential_preflight_require_jq() {
118-
if ! credential_preflight_binary_path jq >/dev/null 2>&1; then
134+
CRED_PREFLIGHT_JQ_BIN="$(credential_preflight_binary_path jq 2>/dev/null || true)"
135+
if [[ -z "$CRED_PREFLIGHT_JQ_BIN" ]]; then
119136
echo "Error: jq is required for acfs credential-preflight" >&2
120137
return 2
121138
fi
122139
}
123140

141+
credential_preflight_jq() {
142+
local jq_bin="$CRED_PREFLIGHT_JQ_BIN"
143+
144+
if [[ -z "$jq_bin" || ! -x "$jq_bin" ]]; then
145+
jq_bin="$(credential_preflight_binary_path jq 2>/dev/null || true)"
146+
[[ -n "$jq_bin" ]] || return 2
147+
CRED_PREFLIGHT_JQ_BIN="$jq_bin"
148+
fi
149+
150+
"$jq_bin" "$@"
151+
}
152+
124153
credential_preflight_abs_path() {
125154
local path="$1"
126155
local dir=""
@@ -253,7 +282,7 @@ credential_preflight_json_array_from_objects() {
253282
if [[ $# -eq 0 ]]; then
254283
printf '[]\n'
255284
else
256-
printf '%s\n' "$@" | jq -s .
285+
printf '%s\n' "$@" | credential_preflight_jq -s .
257286
fi
258287
}
259288

@@ -264,7 +293,7 @@ credential_preflight_add_skipped() {
264293
local reason="$4"
265294
local object=""
266295

267-
object="$(jq -n \
296+
object="$(credential_preflight_jq -n \
268297
--arg file "$(credential_preflight_display_path "$root" "$path")" \
269298
--arg source "$source" \
270299
--arg reason "$reason" \
@@ -293,7 +322,7 @@ credential_preflight_add_finding() {
293322
local evidence="$6"
294323
local object=""
295324

296-
object="$(jq -n \
325+
object="$(credential_preflight_jq -n \
297326
--arg category "$category" \
298327
--arg severity "warning" \
299328
--arg file "$(credential_preflight_display_path "$root" "$path")" \
@@ -508,7 +537,7 @@ credential_preflight_render_json() {
508537

509538
findings_json="$(credential_preflight_json_array_from_objects "${CRED_PREFLIGHT_FINDINGS[@]}")"
510539
skipped_json="$(credential_preflight_json_array_from_objects "${CRED_PREFLIGHT_SKIPPED[@]}")"
511-
categories_json="$(jq -n --argjson findings "$findings_json" '
540+
categories_json="$(credential_preflight_jq -n --argjson findings "$findings_json" '
512541
$findings
513542
| group_by(.category)
514543
| map({category: .[0].category, count: length})
@@ -518,7 +547,7 @@ credential_preflight_render_json() {
518547
status="warn"
519548
fi
520549

521-
jq -n \
550+
credential_preflight_jq -n \
522551
--arg generated_at "$CRED_PREFLIGHT_GENERATED_AT" \
523552
--arg status "$status" \
524553
--argjson files_scanned "$CRED_PREFLIGHT_FILES_SCANNED" \
@@ -563,7 +592,7 @@ credential_preflight_render_human() {
563592
"${#CRED_PREFLIGHT_FINDINGS[@]}" \
564593
"$CRED_PREFLIGHT_FILES_SCANNED"
565594
for object in "${CRED_PREFLIGHT_FINDINGS[@]}"; do
566-
jq -r '"\(.file):\(.line): \(.category) - \(.evidence)\n Remediation: \(.remediation)"' <<<"$object"
595+
credential_preflight_jq -r '"\(.file):\(.line): \(.category) - \(.evidence)\n Remediation: \(.remediation)"' <<<"$object"
567596
done
568597
if [[ ${#CRED_PREFLIGHT_SKIPPED[@]} -gt 0 ]]; then
569598
printf 'Skipped %d file(s); use --json for reasons.\n' "${#CRED_PREFLIGHT_SKIPPED[@]}"

tests/unit/test_credential_preflight.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,35 @@ PASSWORD=abc
176176
pass "benign_examples_pass"
177177
}
178178

179+
test_uses_system_path_not_shadowed_tools() {
180+
local test_dir="$ARTIFACT_DIR/path-shadow"
181+
local fake_bin="$test_dir/bin"
182+
local fixture="$test_dir/.env"
183+
local output="$test_dir/output.json"
184+
local marker="$test_dir/shadowed-tool-ran"
185+
local binary_name=""
186+
187+
mkdir -p "$fake_bin"
188+
write_fixture "$fixture" 'GITHUB_TOKEN=your-token-here'
189+
for binary_name in date dirname jq; do
190+
cat > "$fake_bin/$binary_name" <<'EOF'
191+
#!/usr/bin/env bash
192+
printf 'shadowed tool should not run: %s\n' "$0" > "$CRED_PREFLIGHT_POISON_MARKER"
193+
exit 99
194+
EOF
195+
chmod +x "$fake_bin/$binary_name"
196+
done
197+
198+
CRED_PREFLIGHT_POISON_MARKER="$marker" \
199+
PATH="$fake_bin:/usr/bin:/bin" \
200+
bash "$CREDENTIAL_PREFLIGHT_SH" --json --file "$fixture" > "$output" || return 1
201+
202+
[[ ! -e "$marker" ]] || return 1
203+
jq -e '.status == "pass" and .summary.files_scanned == 1' "$output" >/dev/null || return 1
204+
205+
pass "uses_system_path_not_shadowed_tools"
206+
}
207+
179208
test_detects_hex_encoded_secret_values_under_secret_keys() {
180209
local fixture="$ARTIFACT_DIR/hex-secret/.env"
181210
local output="$ARTIFACT_DIR/hex-secret.json"
@@ -347,6 +376,7 @@ main() {
347376
run_test test_secret_matrix_detects_categories_without_value_leaks
348377
run_test test_detects_private_key_marker
349378
run_test test_benign_examples_pass
379+
run_test test_uses_system_path_not_shadowed_tools
350380
run_test test_detects_hex_encoded_secret_values_under_secret_keys
351381
run_test test_json_secret_value_detection_ignores_unrelated_placeholder_words
352382
run_test test_detects_later_secret_pairs_on_same_line

0 commit comments

Comments
 (0)