Skip to content

fix(install): do not resolve jq/awk through the downloaded release - #8

Open
fxmedus wants to merge 1 commit into
trustabl:mainfrom
fxmedus:fix/dont-trust-downloaded-release-path
Open

fix(install): do not resolve jq/awk through the downloaded release#8
fxmedus wants to merge 1 commit into
trustabl:mainfrom
fxmedus:fix/dont-trust-downloaded-release-path

Conversation

@fxmedus

@fxmedus fxmedus commented Aug 24, 2026

Copy link
Copy Markdown

What

Two changes to scan/trustabl-scan.sh, both about what the script assumes it can trust
from a freshly downloaded release.

1. The extracted tarball shadows system jq and awk

$DEST holds a just-extracted release tarball. Line 108 prepends it to PATH:

tar -xzf "$DEST/$ASSET" -C "$DEST"
export PATH="$DEST:$PATH"

Every later helper in the script then resolves through that PATH first. Lines 145
onward use jq and awk to compute every number the wrapper reports:

RAW_SCORE=$(jq -r '.overall_score // 1' "$JSON_FILE")
SCORE=$(awk -v s="$RAW_SCORE" 'BEGIN{ ... }')
COUNT=$(jq -r '.findings | length // 0' "$JSON_FILE")
MAX_SEV=$(jq -r '...' "$JSON_FILE")

A release asset shipping its own jq would therefore control the readiness score, the
risk score, the finding count and the max severity — the gate decision itself — without
touching the scanner binary. Checksum verification does not help: the checksum attests
that the tarball is the one the release published, not that its contents are only
trustabl.

Fix: bind the binary by path and drop the PATH mutation.

TRUSTABL_BIN="$DEST/trustabl"
...
"$TRUSTABL_BIN" "${BASE_ARGS[@]}" --format sarif > "$SARIF_FILE"
"$TRUSTABL_BIN" "${BASE_ARGS[@]}" --format json  > "$JSON_FILE" || true

Why this is safe: jq is already required before the download exists — the
latest version lookup at line 60 pipes the GitHub API response through it. So jq is
necessarily a system prerequisite and nothing in the script ever depended on the tarball
supplying it. trustabl itself resolves identically, since $DEST was prepended and
already won.

Those two call sites are the only bare trustabl invocations in the file; verified with
a grep after the change.

2. Checksum lookup matches the asset name as a regex

EXPECTED=$(grep " ${ASSET}\$" "$DEST/checksums.txt" | awk '{print $1}' | head -1)

$ASSET is trustabl_${VNUM}_${OS}_${ARCH}.tar.gz and is interpolated straight into a
regex, where its dots are wildcards. An entry that differs from the wanted asset only at
those positions satisfies the match, and head -1 takes whichever comes first.

Demonstrated against a two-line checksums.txt where the decoy has its dots substituted
and underscores left intact:

dead0000  trustabl_1X2X3_linux_amd64Xtar.gz
bbbb2222  trustabl_1.2.3_linux_amd64.tar.gz

old  grep " ${ASSET}\$"   -> dead0000     (wrong entry)
new  awk exact field match -> bbbb2222    (correct entry)

A mismatch here aborts the run rather than passing a bad binary, so the practical
consequence is a confusing failure rather than a bypass. Worth correcting regardless,
since it is the integrity check.

Fix: compare field 2 for equality instead of pattern-matching, allowing sha256sum's
binary-mode * prefix.

EXPECTED=$(awk -v a="$ASSET" '$2 == a || $2 == "*" a { print $1 }' "$DEST/checksums.txt" | head -1)

Verification

bash -n scan/trustabl-scan.sh          -> clean
grep for bare `trustabl ` invocations  -> none remain
grep for `export PATH`                 -> none remain

Scope

One file, +15 / -4. No behaviour change on any input that works today: the same binary
runs, the same checksum is compared, the same exit codes are produced. Nothing outside
the download and invocation path is touched.

Related

Two further fail-open behaviours in this same file are filed separately as an issue,
since hardening either one changes results for existing pipelines and is a maintainer
call rather than something to land unannounced.

export PATH="$DEST:$PATH" put a just-extracted release tarball ahead of the
system PATH, so every later jq and awk resolved out of it. Those compute the
readiness score, risk score, finding count and max severity, so a release asset
shipping its own jq would control the gate decision without touching the
scanner binary.

Bind the binary by path instead. jq is already required before the download
exists (the latest-version lookup uses it), so it is a system prerequisite and
nothing depended on the tarball providing it.

Also match the asset name literally in the checksum lookup: $ASSET contains
dots, which grep treats as wildcards, so an entry differing only at those
positions could be selected.

bash -n clean. No behaviour change for any input that works today.
@sairenchristianbuerano

Copy link
Copy Markdown
Collaborator

Thanks @fxmedus. Flagging that this is being read rather than sitting unlooked-at.

We are reviewing all 52 open PRs together instead of one at a time. 31 of them edit scan/trustabl-scan.sh, so merge order decides as much as any individual verdict — GitHub reports almost all of these as mergeable, but that is each PR against main in isolation, not against each other. The first merge makes most of the rest conflict.

Queued in the security cluster. Resolving jq/awk through the downloaded release path is a real privilege concern and I want to read it properly rather than skim it.

The order we are working to: the test harness in #1 first, since twelve PRs depend on it and nothing is verifiable without it, then the fixes that close fail-open paths, then behaviour changes, then docs. A verdict on this one follows once its cluster is read.

Apologies for the wait, and thanks for the contribution.

@sairenchristianbuerano sairenchristianbuerano left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @fxmedus — approving the substance, but this needs a rebase.

#7 is on main now and rewrote the checksum block, which is where this conflicts
scan/trustabl-scan.sh, around
if curl -fsSL "${AUTH[@]}" -o "$DEST/checksums.txt".

The concern here still stands after that change, and arguably matters more: #7
makes us trust the downloaded archive's integrity, but resolving jq or awk
through the extracted release path means a substituted archive could still supply
the tools we then use to check it. Worth keeping the two separate.

main has the test harness from #1, so bash test/run-tests.sh will verify the
rebase — 19 passing there right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants