git-credential-mosaic: unvalidated identity charset breaks spool JSON and the refusal dedupe #1318

Open
opened 2026-08-19 16:11:36 +00:00 by fred · 0 comments
Collaborator

Two defects in the escalation-spool path of git-credential-mosaic, both from the same root cause: the resolved identity is used unvalidated, and it is arbitrary operator-supplied text (it comes from $MOSAIC_GIT_IDENTITY, git config --get mosaic.gitIdentity, or the stdin username).

Found and reproduced by rev-security-01 during an adversarial review of the deployed helper on a live host. Neither leaks a token value — Q2 of that review scanned every exit path, spool and debug output for the 40-hex token pattern with a working positive control and found zero hits.

F3 — json_escape() does not escape control characters

json_escape() handles backslash, quote, tab, CR and LF. It does not handle the remaining C0 range.

Reproduced: an identity containing \x01 produces a spool line that is not parseable JSON.

This defeats the stated purpose of the function. The comment above it says a spool that silently stops parsing is worse than no spool, because the operator only discovers it while reading the record that explains an outage — which is exactly what happens here.

F4 — a / in the identity breaks the refusal dedupe and leaks a diagnostic

With / in the identity the dedupe marker path cannot be created, so every refusal appends a record: three runs produced three spool lines and zero markers. The cap that is supposed to bound spool growth does not engage.

Separately, : > $dedupe 2>/dev/null leaks a bash error to stderr. The redirection is evaluated before the suppressor engages, so the error escapes and exposes the spool path and the raw identity. No secret value.

Suggested fix

Validate the resolved identity against [A-Za-z0-9._-]+ and refuse anything else. This covers both defects at the source and is strictly narrower than what a Gitea login can be, so it rejects nothing legitimate.

Escaping U+0000U+001F in json_escape() is worth doing regardless, as defence in depth for any field that is not the identity.

What is NOT a defect here

The same review asked whether the helper verifies its caller. It does not — naming any seat emits that seat's token. On a single-uid host that is not a boundary the helper can enforce, since any process running as that uid can read any token file directly. It is an attribution control, not an isolation boundary, and it is unchanged from before #1311. Recording it so it is not re-reported as a finding.

Traversal, empty, whitespace, prefix and suffix identities all fail closed correctly.

Two defects in the escalation-spool path of `git-credential-mosaic`, both from the same root cause: the resolved identity is used unvalidated, and it is arbitrary operator-supplied text (it comes from `$MOSAIC_GIT_IDENTITY`, `git config --get mosaic.gitIdentity`, or the stdin username). Found and reproduced by `rev-security-01` during an adversarial review of the deployed helper on a live host. Neither leaks a token value — `Q2` of that review scanned every exit path, spool and debug output for the 40-hex token pattern with a working positive control and found zero hits. ## F3 — `json_escape()` does not escape control characters `json_escape()` handles backslash, quote, tab, CR and LF. It does not handle the remaining C0 range. **Reproduced:** an identity containing `\x01` produces a spool line that is not parseable JSON. This defeats the stated purpose of the function. The comment above it says a spool that silently stops parsing is worse than no spool, because the operator only discovers it while reading the record that explains an outage — which is exactly what happens here. ## F4 — a `/` in the identity breaks the refusal dedupe and leaks a diagnostic With `/` in the identity the dedupe marker path cannot be created, so **every refusal appends a record**: three runs produced three spool lines and zero markers. The cap that is supposed to bound spool growth does not engage. Separately, `: > $dedupe 2>/dev/null` leaks a bash error to stderr. The redirection is evaluated before the suppressor engages, so the error escapes and exposes the spool path and the raw identity. No secret value. ## Suggested fix Validate the resolved identity against `[A-Za-z0-9._-]+` and refuse anything else. This covers both defects at the source and is strictly narrower than what a Gitea login can be, so it rejects nothing legitimate. Escaping `U+0000`–`U+001F` in `json_escape()` is worth doing regardless, as defence in depth for any field that is not the identity. ## What is NOT a defect here The same review asked whether the helper verifies its caller. It does not — naming any seat emits that seat's token. On a single-uid host that is not a boundary the helper can enforce, since any process running as that uid can read any token file directly. It is an attribution control, not an isolation boundary, and it is unchanged from before #1311. Recording it so it is not re-reported as a finding. Traversal, empty, whitespace, prefix and suffix identities all fail closed correctly.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1318