Skip to content

fix: stop DEBUG from printing the GitHub token into the build log - #20

Open
bradAGI wants to merge 2 commits into
trustabl:mainfrom
bradAGI:fix/redact-github-token-under-debug
Open

fix: stop DEBUG from printing the GitHub token into the build log#20
bradAGI wants to merge 2 commits into
trustabl:mainfrom
bradAGI:fix/redact-github-token-under-debug

Conversation

@bradAGI

@bradAGI bradAGI commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Depends on #1 (the test suite) — it is the first commit here so CI can run. Review the second commit.

The leak

DEBUG=true runs the whole script under set -x, and bash echoes each command after expansion. Three of those commands carry GITHUB_TOKEN:

+ AUTH=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
+ curl -fSL -H 'Accept: application/octet-stream' -H 'Authorization: Bearer ghp_TESTSECRETVALUE' -o ... 
+ curl -fsSL -H 'Authorization: Bearer ghp_TESTSECRETVALUE' -o ...

CodeBuild and CodeCatalyst logs are readable by anyone with access to the project, and they persist for the log group's retention. So a credential the README explicitly tells people to store as a secret ends up in plaintext beside the scan output — and the person who turned on DEBUG was debugging a scan, not thinking about credentials.

The fix

The token is now expanded in exactly one place — auth_curl — which saves xtrace, disables it for the call, and restores it.

The AUTH array could not be made safe and is gone. That is the part worth noting: holding the header in a variable does not help, because the assignment is traced too. The header is now built inside the wrapper from GITHUB_TOKEN directly, so there is no traced line anywhere that expands it.

xtrace is saved and restored rather than switched off wholesale, so DEBUG still does its job.

Measured

Same scan, DEBUG=true GITHUB_TOKEN=ghp_TESTSECRETVALUE, before and after:

token occurrences trace lines
main 3 287
this PR 0 289

Tracing is intact — the calls still appear, just without the header:

+ auth_curl -fSL -H 'Accept: application/octet-stream' -o .../trustabl_9.9.9_darwin_arm64.tar.gz https://github.com/...
+ auth_curl -fsSL -o .../checksums.txt https://github.com/.../checksums.txt

Test

The new test asserts both halves, so it cannot pass vacuously:

assert_contains "trace" "$SCAN_OUT" "+ "                        # set -x is still on
assert_not_contains "trace" "$SCAN_OUT" "ghp_TESTSECRETVALUE"   # and the token is not in it

Before: not ok — trace: output unexpectedly contained 'ghp_TESTSECRETVALUE'. After: all 16 test(s) passed.

Related: #19 documents DEBUG and, until this lands, warns about exactly this. If both merge, that note can be softened.

The scanner is the whole product here — it resolves a release, verifies it,
runs the engine, scales the score, and decides whether the build fails — and
until now nothing checked any of it. A regression in the gate logic or in the
jq that reads the engine's ScanResult would ship silently.

The suite drives the real scan/trustabl-scan.sh. Each test builds an actual
gzipped release tarball containing a stub engine and a real checksums.txt over
it, then puts a stub curl on PATH that serves that directory by URL basename.
The download, sha256 verification, extraction and invocation therefore all run
unmodified; only the network and the engine binary are substituted.

The fixtures are unmodified output from a real `trustabl scan` (engine v0.1.7),
so the assertions pin the scanner against the ScanResult shape the engine
actually emits rather than a hand-written approximation of it.
DEBUG=true runs the whole script under set -x, and bash echoes each command
after expansion. Three of those commands carried GITHUB_TOKEN:

  + AUTH=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
  + curl -fSL -H 'Accept: application/octet-stream' \
      -H 'Authorization: Bearer ghp_TESTSECRETVALUE' ...
  + curl -fsSL -H 'Authorization: Bearer ghp_TESTSECRETVALUE' ...

CodeBuild and CodeCatalyst logs are readable by anyone with access to the
project, so a token the README tells people to store as a secret ends up in
plaintext next to the scan output — and stays there for the log's retention.

The token is now expanded in exactly one place: auth_curl, which saves xtrace,
turns it off for the duration of the call, and restores it. Holding the header
in an AUTH array could not be made safe, because the assignment is traced too,
so the array is gone and the header is built inside the wrapper from
GITHUB_TOKEN directly.

xtrace is restored rather than left off, so DEBUG still traces everything else:
287 trace lines before, 289 after, with token occurrences going 3 -> 0.

@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.

Confirmed the bug on main: set -x at scan/trustabl-scan.sh:30, the token expanded at :39, and three curl calls at :60, :85, :90. Under xtrace bash echoes commands after expansion, so DEBUG=true writes the credential in plaintext to CloudWatch Logs on CodeBuild and to the run output on CodeCatalyst. Both persist past the build. Your description of the four copies matches what the script actually does.

The auth_curl approach is the right one. The token is expanded in exactly one place, and because nothing is written to disk there is no cleanup path that can be missed on an abnormal exit. case "$-" in *x*) is also the correct way to check xtrace state rather than inferring it from DEBUG, which keeps working if something else turns tracing on later. The call site stays traced, so DEBUG loses almost nothing.

The regression test is what makes this mergeable rather than just correct:

assert_not_contains "trace" "$SCAN_OUT" "ghp_TESTSECRETVALUE"

That is the test that stops this coming back.

One nit, non-blocking: set -e is active at that point, so a failing curl exits before rc=$? runs and xtrace is never restored. The shell is on its way out so it does not matter in practice, but the rc capture is dead code on that path.

One process note rather than a code comment: this carries #1 and roughly 1,100 lines of test harness with it. I would rather #1 were reviewed on its own merits than merged as a side effect of a security fix. Happy for both to land, just not for #1 to arrive unexamined.

Before this merges, please pull in the README paragraph from #40 about storing GITHUB_TOKEN via Secrets Manager or Parameter Store rather than as a plaintext environment variable. It covers an exposure this code change does not.

sairenchristianbuerano added a commit that referenced this pull request Aug 25, 2026
This repo had no CI while taking external pull requests, so every incoming
change was reviewed by eye with nothing verifying it. That is also why branch
protection here could not require a status check.

Three gates: bash -n across every script, shellcheck at -S error, and a YAML
parse of buildspec.yml and the CodeCatalyst workflow - the two files AWS
actually consumes, where a malformed one fails in a customer's pipeline rather
than here.

shellcheck runs at error severity only. A required check that argues about
quoting preferences gets disabled within a week.

Named lint.yml rather than ci.yml so it coexists with the test harness in #20
instead of colliding with it.
@sairenchristianbuerano

Copy link
Copy Markdown
Collaborator

Thanks @bradAGI. Approving.

Confirmed on main: set -x at line 30, the token expanded at line 39, and three curl calls carrying it. Under xtrace bash echoes commands after expansion, so DEBUG=true writes the credential in plaintext to CloudWatch on CodeBuild and to the run output on CodeCatalyst. Both persist well past the build.

case "$-" in *x*) is the correct state check rather than inferring from DEBUG, and the call site stays traced so debuggability barely suffers. The regression test is what makes this mergeable rather than merely correct.

One nit, non-blocking: set -e is active there, so a failing curl exits before rc=$? runs and xtrace is never restored. Harmless, since the shell is leaving anyway, but that capture is dead on the failure path.

@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 @bradAGI — approving the substance, but it needs a rebase.

Two conflicts now that #7 and #11 are on main:

  • scan/trustabl-scan.sh#7 rewrote the checksum block your auth_curl
    wrapper wraps, around if auth_curl -fsSL -o "$DEST/checksums.txt"
  • test/run-tests.sh#11 added tests at the same spot as
    test_debug_does_not_echo_the_github_token. That one is additive; both sets
    of tests should survive

Worth flagging on the test file: git splits the conflict mid-function there, so a
naive "keep both sides" silently drops a closing brace and bash -n only
complains 30 lines later. I hit exactly that resolving #21. Run
bash test/run-tests.sh after — all 19 pass on main today, and you should see
20 with yours.

The fix itself is unchanged and still correct: DEBUG=true puts the token in
CloudWatch four times over, and the regression test is what makes it stick.

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