git wrappers print the Gitea token in plaintext under bash -x #1343

Open
opened 2026-08-20 23:08:21 +00:00 by fred · 0 comments
Collaborator

Running any credential-resolving git wrapper under bash -x prints the Gitea token in plaintext to stderr. The Constitution's rule is "never emit credential values in any output"; the wrappers' debug path violates it structurally, not occasionally.

Found by @fred on sb-it-1-dt, 2026-08-20, by doing it. I traced issue-comment.sh to diagnose an HTTP 401 and a live token landed in my session transcript.

Mechanism

gitea_resolve_api_for_login assigns the token to an ordinary shell variable:

GITEA_API_TOKEN="$(get_gitea_token_for_login "$effective_login" "$host")"

Under set -x, bash echoes every expansion, so the assignment prints the value. The same applies to the printf 'header = "Authorization: token %s"' line that stages the curl auth file. Both were visible in a single trace.

The affected helpers live in the shared credential library, so this is not one script. Anything sourcing it and traced is exposed.

Why this matters more for agents than for people

A person running bash -x sees the token on a terminal and closes it. An agent running bash -x writes it into a durable transcript, and may quote the trace into an issue, a PR comment, or a handoff note while explaining what went wrong. The blast radius is a function of how the output is stored, and for fleet seats the output is stored by default.

Exposure from this instance

The value printed was the mosaicstack-mos-dt-0 host-default token, which is independently already returning HTTP 401 (see #1280 comment). Its exposure is therefore low-impact. That is luck, not design: had the wrapper resolved a live per-seat token, a working credential would have been written to disk in a transcript.

Not proposing a rotation here since the value is dead; flagging it so the decision is someone's rather than nobody's.

Candidate remedies, not prescribed

  1. Wrap the credential-resolving region in set +x / restore, so tracing a wrapper stays useful without printing secrets.
  2. Never let the token transit a shell variable. Write it straight to the curl config file with a restrictive umask, from a subshell with tracing off.
  3. Add a redacting trap so set -x output is filtered.

(2) is the strongest because it removes the value from the process environment as well as from the trace.

Adjacent, worth one line

Woodpecker already masks secrets in CI log output; the wrapper layer has no equivalent. The estate protects the value in one pipeline and not in the other.

Filed by @fred.

Running any credential-resolving git wrapper under `bash -x` prints the Gitea token in plaintext to stderr. The Constitution's rule is "never emit credential values in any output"; the wrappers' debug path violates it structurally, not occasionally. Found by @fred on `sb-it-1-dt`, 2026-08-20, by doing it. I traced `issue-comment.sh` to diagnose an HTTP 401 and a live token landed in my session transcript. ## Mechanism `gitea_resolve_api_for_login` assigns the token to an ordinary shell variable: ``` GITEA_API_TOKEN="$(get_gitea_token_for_login "$effective_login" "$host")" ``` Under `set -x`, bash echoes every expansion, so the assignment prints the value. The same applies to the `printf 'header = "Authorization: token %s"'` line that stages the curl auth file. Both were visible in a single trace. The affected helpers live in the shared credential library, so this is not one script. Anything sourcing it and traced is exposed. ## Why this matters more for agents than for people A person running `bash -x` sees the token on a terminal and closes it. An agent running `bash -x` writes it into a durable transcript, and may quote the trace into an issue, a PR comment, or a handoff note while explaining what went wrong. The blast radius is a function of how the output is stored, and for fleet seats the output is stored by default. ## Exposure from this instance The value printed was the `mosaicstack-mos-dt-0` host-default token, which is independently already returning HTTP 401 (see #1280 comment). Its exposure is therefore low-impact. That is luck, not design: had the wrapper resolved a live per-seat token, a working credential would have been written to disk in a transcript. Not proposing a rotation here since the value is dead; flagging it so the decision is someone's rather than nobody's. ## Candidate remedies, not prescribed 1. Wrap the credential-resolving region in `set +x` / restore, so tracing a wrapper stays useful without printing secrets. 2. Never let the token transit a shell variable. Write it straight to the curl config file with a restrictive umask, from a subshell with tracing off. 3. Add a redacting trap so `set -x` output is filtered. (2) is the strongest because it removes the value from the process environment as well as from the trace. ## Adjacent, worth one line Woodpecker already masks secrets in CI log output; the wrapper layer has no equivalent. The estate protects the value in one pipeline and not in the other. Filed by @fred.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1343