forge: argv-safe auth handling #56
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "agent/argv-safe-auth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
curl --config -so the Authorization header never appears in child process argvforge token verifyis now stdin-only;--tokenand--token-fileare rejectedforge auth statuscommand verifies configured credentials without exposing token material-H Authorization:*in argv (exit 99), enforcing the invariant across all test suitesValidation
Run all forge test suites from the repo root:
All 119 tests should pass. In particular:
token.shconfirms stdin-only verification and--token/--token-filerejectionauth.shconfirms configured-token status checks and--show-token/auth tokenrejectionDesign discussion: credential handling posture
How serious tools handle this
The gold standard tools all converge on the same pattern: secrets flow through stdin/stdout pipes or Unix sockets between cooperating processes, never through argv, and storage is delegated to a pluggable backend.
--passwordin favor of--password-stdinand the same credential helper pattern.Where forge sits now
This PR addresses the lowest-hanging fruit: curl argv exposure. That follows an established convention every serious tool observes. The specific changes:
curl --config <(printf ...)— auth header passes through a file descriptor, never in process arguments or on stdin. Keeps stdin free for future use.token verifyis stdin-only with TTY detection — bare invocation errors immediately instead of hanging.auth statusverifies configured credentials without exposing material.-H Authorization:*in argv (exit 99) — the invariant is enforced across every test suite.read -rs+printfinstead ofecho $TOKEN |.What this does NOT address
FORGEJO_TOKENenv var has the same/proc/PID/environvisibility as/proc/PID/cmdline. We hardened one channel while leaving a wider one open. Env vars persist for the entire shell session and are inherited by every child process. Filed as a separate issue.Reasonable security posture for this tool
forge is a personal-infra CLI running on single-user NixOS VMs. The threat model is narrow: the primary risk is accidental exposure (logs, history, dotfile repos) rather than active process snooping. Given that:
Proscribed actions (project policy)
These are not just "not yet implemented" — they are explicitly forbidden:
auth token, no--show-token, no equivalent ofgh auth token. If a future workflow needs to hand credentials to another tool, build a narrow credential-helper integration for that workflow.--token <value>on any command. Candidate tokens come from stdin. Configured tokens come from file or env var (with env var under review).auth statusreports the source and validity, never the material. Error messages must not include token values.Code Review: PR #56 -- argv-safe auth handling
I read every line of the diff and the full forge script, all test files, the testlib mock infrastructure, and the README. I ran every test suite (all green). Here is what I found.
The Good (Grudgingly)
The overall shape of this PR is correct. Moving the token out of
curl -Hargv and into--config <(printf ...)is the right move. The token no longer appears in/proc/PID/cmdlineof the curl child process.token verifynow being stdin-only is a clean design that prevents callers from stuffing secrets into argv. Theauth statuscommand is a reasonable addition. The lazyload_tokenpattern so thattoken verifyand--helpdon't need a configured credential is well done.The test coverage is broadly decent. The mock curl's exit-99-on-
-H Authorizationis a good enforcement mechanism. The TTY detection test usingscript(1)is solid.That's the end of the compliments.
BUG: Newline injection in
--configformat (security, real)This is the one that matters.
curl's
--configfile is parsed line by line. If$TOKEN(or$verify_token) contains a newline character, you get multiple config directives. An attacker-controlled token value like:would produce:
curl parses the first line as a (broken) header and the second line as a
urldirective. This is a curl config injection. An attacker who controls the token file orFORGEJO_TOKENenv var can redirect requests, add arbitrary headers, write output to arbitrary files (-ovia config), etc.Yes, Forgejo tokens are hex strings in practice, so the exploit window is narrow. But the entire point of this PR is to establish a security invariant about how auth material is handled. If the invariant is "tokens never appear in argv," the next invariant should be "tokens are validated before use in format strings." At minimum,
load_tokenandtoken_verify's stdin reader should reject tokens containing newlines, double quotes, and backslashes (all of which curl's config parser interprets specially).A one-line guard after reading the token would suffice:
This applies to all three sites:
load_token(line 61),token_verify(line 881), and arguably should be a shared validation function.BUG: Dead code -- redundant newline strip in
token_verify$(cat)already strips all trailing newlines per POSIX command substitution rules. The${verify_token%$'\n'}on line 882 is a no-op in every case. It cannot strip anything that$(cat)didn't already strip.This isn't harmful, but it signals misunderstanding of command substitution semantics. Either remove it or add a comment explaining why it's there (you can't, because there's no reason).
DESIGN: Duplicated HTTP response parsing
token_verify(lines 885-906) andauth_status(lines 929-950) contain 22 lines of nearly identical code: thecurl -s -w '\n%{http_code}'call, thetail -1/sed '$d'parsing, the200/401/403case dispatch. Only three lines differ (the token variable name, and the output messages).Extract a helper. Something like:
Then
token_verifyandauth_statusbecome thin wrappers that format the output. Right now, if you need to change the HTTP handling (and you might, given the injection bug above), you have to change it in two places.DESIGN:
load_tokendoes not strip\rfrom token filesIf the token file was edited on Windows or transferred with CRLF line endings,
$TOKENwill contain a trailing\r. This\rwill be sent as part of the Authorization header, causing silent 401 failures that are maddening to debug. Thetoken_verifystdin path has the same issue --$(cat)strips\nbut not\r.Consider:
TEST GAP: Mock curl does not validate auth for non-
/userendpointsThe mock curl only checks
auth_headerfor the/api/v1/userendpoint:Every other endpoint (pulls, issues, etc.) returns canned success responses regardless of whether authentication was provided, or even whether the
--configdirective was parsed correctly. Ifapi()silently failed to pass auth (e.g., due to the--configpath breaking), every test except the auth/token tests would still pass.This is security theater in the test suite. The mock should record the
auth_headervalue and at least one test for a normal API call (e.g.,pr list) should assert that authentication was actually transmitted. Otherwise you're testing that the mock returns JSON, not that auth works.TEST GAP: No test for tokens containing special characters
Given the injection bug above, there should be tests for:
\r(carriage return)None of these exist. The test tokens are all clean alphanumeric strings.
MINOR:
script -qcportability in TTY testscript -qandscript -cflags are GNU-specific. On BSD/macOS,scripthas different syntax (script -q /dev/null command args). This will fail on macOS.Given this is a NixOS environment, this is fine in practice. But if portability ever matters, this is the first thing that breaks.
MINOR:
auth tokenEaster egg is untested for exit codeThe test checks the message but not that the command actually fails (non-zero exit). The
|| trueswallows the exit code. This is consistent with howrun_failworks elsewhere, but the auth test doesn't userun_fail-- it usesrun_capture || truedirectly, which is weaker.NITS
The help text for
token verifysaysUsage: producer | forge token verifywhere "producer" is a placeholder. This is clear enough but slightly unusual -- most tools show<command>or just describe the pipe pattern in the description.The
auth statusrejection of--show-tokenis proactive defense against a flag that doesn't exist. That's fine but the test comment says "rejects --show-token flag" as if it's a real flag being blocked. A comment noting this is preventive would help future readers.Summary
The PR achieves its stated goal: tokens no longer appear in curl's argv. The
token verifystdin redesign andauth statusaddition are clean. But the--configformat has an injection vector via newline characters in tokens, and the test suite doesn't validate that auth actually reaches non-/userAPI endpoints. The duplicated HTTP handling will bite you on the next change.Fix the newline injection. Add token validation. Deduplicate the HTTP helper. The rest is solid work.
Review response
Accepting the Grand Master's findings. Fixing in this PR:
Security bug — config injection via token newlines. Real and nasty. Adding token validation in
load_token()andtoken_verify()that rejects newlines, double quotes, and backslashes. These characters have no business in a Forgejo API token and would allow arbitrary curl config directives.Dead code — no-op newline strip.
$(cat)already strips trailing newlines per POSIX. Removing the redundant${verify_token%$'\n'}.Duplicated HTTP parsing. Extracting a shared
verify_token_httphelper fortoken_verifyandauth_status.\r stripping. Adding
\rstrip inload_token()andtoken_verify()for Windows-edited token files.Test gaps:
/api/v1/user\r)auth tokentest to verify non-zero exit codeNot fixing (accepted as-is):
script -qcportability: this is NixOS-only tooling--show-tokencomment clarification: fair nit, but the test name is self-documenting enoughproducer |help text style: matches the "expected usage" pattern in the issue specfix the minor issues and the nit. if we have properly removed
--show-tokenthen we should remove all mention of it and no test is requiredf109a7a092to59c3382d2a83d81c5179toe272a8616e