From df7b5f4857db4e2393e0e14763374a5001ce6403 Mon Sep 17 00:00:00 2001 From: Sheel Patel Date: Mon, 24 Aug 2026 16:15:50 -0400 Subject: [PATCH 1/3] fix(scan): exit 2 when setup fails, not exit 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/EVALUATION.md says exit 1 is a gate result and exit 2 is a scanner/I/O failure. Resolving the release, an unsupported platform, or a checksum mismatch is the latter — not a findings gate. Co-authored-by: Cursor --- scan/trustabl-scan.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scan/trustabl-scan.sh b/scan/trustabl-scan.sh index caa6314..ad251ce 100644 --- a/scan/trustabl-scan.sh +++ b/scan/trustabl-scan.sh @@ -61,7 +61,7 @@ if [ "$VER" = "latest" ]; then fi if [ -z "$VER" ] || [ "$VER" = "null" ]; then echo "Could not resolve trustabl version. Pin 'VERSION' to a tag, or set GITHUB_TOKEN." - exit 1 + exit 2 fi echo "Trustabl version: $VER" @@ -70,12 +70,12 @@ VNUM="${VER#v}" case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=darwin ;; - *) echo "Unsupported OS $(uname -s)"; exit 1 ;; + *) echo "Unsupported OS $(uname -s)"; exit 2 ;; esac case "$(uname -m)" in x86_64|amd64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; - *) echo "Unsupported arch $(uname -m)"; exit 1 ;; + *) echo "Unsupported arch $(uname -m)"; exit 2 ;; esac ASSET="trustabl_${VNUM}_${OS}_${ARCH}.tar.gz" DEST="$(pwd)/.trustabl-bin" @@ -94,7 +94,7 @@ if curl -fsSL "${AUTH[@]}" -o "$DEST/checksums.txt" \ ACTUAL=$(sha256sum "$DEST/$ASSET" | awk '{print $1}') if [ "$EXPECTED" != "$ACTUAL" ]; then echo "Checksum mismatch for $ASSET: expected $EXPECTED, got $ACTUAL" - exit 1 + exit 2 fi echo "checksum verified: $ASSET" else From cabf505f57ec6ce956c343316f0516f23c3be8cd Mon Sep 17 00:00:00 2001 From: Sheel Patel Date: Mon, 24 Aug 2026 16:18:10 -0400 Subject: [PATCH 2/3] test: prove unresolved VERSION exits 2; leave checksum alone Checksum mismatch is already in several fail-closed PRs. Keep this change to setup/I/O that #3 does not cover, and add an offline test. Co-authored-by: Cursor --- scan/trustabl-scan.sh | 2 +- test/test-setup-exit.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100755 test/test-setup-exit.sh diff --git a/scan/trustabl-scan.sh b/scan/trustabl-scan.sh index ad251ce..8797a70 100644 --- a/scan/trustabl-scan.sh +++ b/scan/trustabl-scan.sh @@ -94,7 +94,7 @@ if curl -fsSL "${AUTH[@]}" -o "$DEST/checksums.txt" \ ACTUAL=$(sha256sum "$DEST/$ASSET" | awk '{print $1}') if [ "$EXPECTED" != "$ACTUAL" ]; then echo "Checksum mismatch for $ASSET: expected $EXPECTED, got $ACTUAL" - exit 2 + exit 1 fi echo "checksum verified: $ASSET" else diff --git a/test/test-setup-exit.sh b/test/test-setup-exit.sh new file mode 100755 index 0000000..0f70817 --- /dev/null +++ b/test/test-setup-exit.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Offline: unresolved VERSION is I/O, so the wrapper must exit 2, not 1. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +STUB="$(mktemp -d)" +trap 'rm -rf "$STUB"' EXIT + +cat > "$STUB/curl" <<'EOF' +#!/usr/bin/env bash +echo '{"tag_name":""}' +exit 0 +EOF +chmod +x "$STUB/curl" + +export PATH="$STUB:$PATH" +export VERSION=latest +set +e +bash "$ROOT/scan/trustabl-scan.sh" >/tmp/trustabl-setup-exit.log 2>&1 +rc=$? +set -e +[ "$rc" -eq 2 ] || { + echo "expected exit 2 when VERSION cannot be resolved, got $rc" + cat /tmp/trustabl-setup-exit.log + exit 1 +} +echo "ok: unresolved VERSION exits 2" From 60979864a137d27ea5fe2a59d67b98975ca1bf8c Mon Sep 17 00:00:00 2001 From: Sheel Patel Date: Tue, 25 Aug 2026 02:41:22 -0400 Subject: [PATCH 3/3] fix(scan): reject VERSION that leaves the download path Co-authored-by: Cursor --- scan/trustabl-scan.sh | 7 ++++ test/test-setup-exit.sh | 73 +++++++++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/scan/trustabl-scan.sh b/scan/trustabl-scan.sh index 8797a70..ee0917f 100644 --- a/scan/trustabl-scan.sh +++ b/scan/trustabl-scan.sh @@ -63,6 +63,13 @@ if [ -z "$VER" ] || [ "$VER" = "null" ]; then echo "Could not resolve trustabl version. Pin 'VERSION' to a tag, or set GITHUB_TOKEN." exit 2 fi +# One path segment only. A slash, '..', or URL metacharacter in a pin or in +# tag_name from /releases/latest would change +# /releases/download/${VER}/${ASSET} and the local -o filename. +if [[ "$VER" == */* || "$VER" == *\\* || "$VER" == *..* || "$VER" == *[[:space:]]* || "$VER" == *:* || "$VER" == *@* ]]; then + echo "Invalid trustabl version. Pin VERSION to a single release tag." + exit 2 +fi echo "Trustabl version: $VER" # ---- install the release binary ---- diff --git a/test/test-setup-exit.sh b/test/test-setup-exit.sh index 0f70817..9521054 100755 --- a/test/test-setup-exit.sh +++ b/test/test-setup-exit.sh @@ -1,10 +1,28 @@ #!/usr/bin/env bash -# Offline: unresolved VERSION is I/O, so the wrapper must exit 2, not 1. +# Offline: unresolved or path-breaking VERSION is setup I/O, so exit 2. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" STUB="$(mktemp -d)" -trap 'rm -rf "$STUB"' EXIT +LOG="$(mktemp)" +trap 'rm -rf "$STUB" "$LOG"' EXIT +run_scan() { + set +e + PATH="$STUB:$PATH" bash "$ROOT/scan/trustabl-scan.sh" >"$LOG" 2>&1 + rc=$? + set -e +} + +expect_exit_2() { + local why="$1" + [ "$rc" -eq 2 ] || { + echo "expected exit 2 ($why), got $rc" + cat "$LOG" + exit 1 + } +} + +# ---- unresolved latest ---- cat > "$STUB/curl" <<'EOF' #!/usr/bin/env bash echo '{"tag_name":""}' @@ -12,15 +30,44 @@ exit 0 EOF chmod +x "$STUB/curl" -export PATH="$STUB:$PATH" -export VERSION=latest -set +e -bash "$ROOT/scan/trustabl-scan.sh" >/tmp/trustabl-setup-exit.log 2>&1 -rc=$? -set -e -[ "$rc" -eq 2 ] || { - echo "expected exit 2 when VERSION cannot be resolved, got $rc" - cat /tmp/trustabl-setup-exit.log - exit 1 -} +VERSION=latest run_scan +expect_exit_2 "unresolved VERSION" echo "ok: unresolved VERSION exits 2" + +# ---- pin that would leave /releases/download/${VER}/ ---- +cat > "$STUB/curl" <<'EOF' +#!/usr/bin/env bash +echo "CURL_RAN $*" >&2 +exit 0 +EOF +chmod +x "$STUB/curl" + +VERSION='v1.0.0/../../../tmp/pwn' run_scan +expect_exit_2 "VERSION path breakout" +if grep -q CURL_RAN "$LOG"; then + echo "curl must not run when VERSION leaves the download path" + cat "$LOG" + exit 1 +fi +echo "ok: VERSION with slash exits 2 without fetching" + +# ---- tag_name from /releases/latest that would leave the path ---- +cat > "$STUB/curl" <<'EOF' +#!/usr/bin/env bash +if printf '%s' "$*" | grep -q 'releases/download'; then + echo "DOWNLOAD_RAN $*" >&2 + exit 0 +fi +echo '{"tag_name":"v1.0.0/../evil"}' +exit 0 +EOF +chmod +x "$STUB/curl" + +VERSION=latest run_scan +expect_exit_2 "latest tag_name path breakout" +if grep -q DOWNLOAD_RAN "$LOG"; then + echo "binary fetch must not run when latest tag_name leaves the download path" + cat "$LOG" + exit 1 +fi +echo "ok: latest tag_name with slash exits 2 without downloading"