Hello,
I believe I've hit a bug in contrib/credential/libsecret that leaks the secret to stdout on `store` (and, by inspection of the same code path, `erase`). It reproduces on the current 2.53.0 Arch package and the relevant code path is unchanged on master as of today.
Summary -------
`git-credential-libsecret store` unconditionally echoes the `username` and `password` from its parsed input back to stdout after the store operation completes. This exposes the secret to whatever consumes the helper's stdout -- terminal scrollback, shell pipelines, CI logs, or any parent-process capture -- whenever a caller feeds credentials in via pipe (the documented non-interactive seeding pattern).
`get` should write credentials to stdout. `store` and `erase` should not.
Affected version ----------------
- Reproduced on git 2.53.0-1.1 (Arch Linux community/git). - Source inspected from the installed package (/usr/share/git/credential/libsecret/git-credential-libsecret.c) and confirmed against contrib/credential/libsecret/git-credential-libsecret.c on origin/master.
Reproduction ------------
$ printf "protocol=https\nhost=example.invalid\nusername=alice\npassword=SECRET123\n\n" | /usr/lib/git-core/git-credential-libsecret store username=alice password=SECRET123 $ echo $? 0
Only `username=` and `password=` are echoed (not `protocol=` / `host=`), matching the four fields `credential_write()` emits.
Expected behaviour ------------------
`store` produces no stdout on success. Exit code unchanged.
Root cause ----------
In main(), the write is unconditional after the op dispatch:
ret = credential_read(&cred); if (ret) goto out;
/* perform credential operation */ ret = (*try_op->op)(&cred);
credential_write(&cred); /* unconditional for get/store/erase */
`credential_write()` emits `username`, `password`, `password_expiry_utc`, and `oauth_refresh_token` to stdout. That is correct for `get` (returning the looked-up credential) and incorrect for `store` / `erase`, where the struct still holds the just-read stdin input.
Comparable helpers in the same tree should probably be audited for the same pattern; at least `credential-store` historically only writes on `get`.
Proposed fix ------------
Minimal change -- guard the write:
ret = credential_read(&cred); if (ret) goto out;
/* perform credential operation */ ret = (*try_op->op)(&cred);
- credential_write(&cred); + if (!strcmp(argv[1], "get")) + credential_write(&cred);
A cleaner refactor would add a `writes_output` flag or a `write_result` callback to `struct credential_operation` so each op declares its own output contract, but the guard above is the smallest safe change. Happy to turn it into a proper patch with sign-off if that's preferred.
Security impact ---------------
The documented pattern for seeding credentials non-interactively is:
printf "protocol=...\nhost=...\nusername=...\npassword=...\n\n" | git-credential-<helper> store
Running this at a terminal prints the secret into scrollback. Running it in a shell script whose stdout goes to a log file persists the secret in that log. Running it in CI captures the secret in the pipeline artefact. Every real-world use of the documented pattern is affected.
Severity is moderate: the leak requires the user to run a legitimate command -- no attacker-controlled input path -- but the leak happens on the "correct" documented workflow, silently, with exit code 0.
Workaround ----------
Redirect stdout explicitly:
printf "...\n" | /usr/lib/git-core/git-credential-libsecret store >/dev/null
Environment -----------
- Distribution: CachyOS (Arch Linux derivative) - Kernel: Linux 7.0.0-1-cachyos - git: 2.53.0-1.1 - Shell: bash (invoked from a fish login shell)
Happy to coordinate disclosure if preferred, but the workaround is trivial, the patch is one line, and the bug affects any scripted credential seeding -- so there's little to gain from embargo.
Thanks,
Lutz-Christian Quander