Skip to content

fix(bin): send the Relay bearer to curl on stdin instead of a temp file - #2892

Open
cedmos wants to merge 2 commits into
kunchenguid:mainfrom
cedmos:fm/fmx-bearer-argv-leak
Open

fix(bin): send the Relay bearer to curl on stdin instead of a temp file#2892
cedmos wants to merge 2 commits into
kunchenguid:mainfrom
cedmos:fm/fmx-bearer-argv-leak

Conversation

@cedmos

@cedmos cedmos commented Aug 23, 2026

Copy link
Copy Markdown

Intent

Keep the Bearer key out of curl argv in the Relay scripts.

THE DEFECT AS BRIEFED: process arguments are world-readable via ps, so -H "Authorization: Bearer $key" exposes the credential to any local process for the life of the call. A standing captain rule from today makes the ONLY allowed shape 'curl -K -', reading url and header from stdin.

IMPORTANT PREMISE CORRECTION FOUND WHILE WORKING (deliberate, not an oversight): the fm-x-* scripts were NOT passing the bearer in argv. All three request sites already routed the header through -H "@tempfile" (a mode-600 temp file written by fmx_auth_header_file). There is no -H "Authorization: Bearer $key" anywhere in this family. This was verified by tracing every FMX_TOKEN reference and every curl call site. The work was still done because the captain's rule mandates the 'curl -K -' shape regardless, and it is a genuine improvement: it removes a credential from disk whose cleanup depended on traps, and traps do not survive SIGKILL. Reviewers should not expect to find a removed argv leak in this diff.

SCOPE FENCE (non-negotiable, explicitly imposed): fix ONLY the bin/fm-x-*.sh family. Do NOT touch, for any reason: bin/flowy-fleet-watch.sh and bin/flowy-fleet-nudge.sh (another owner runs these), bin/fm-completion-outbox.sh (a live crew is fixing exactly this defect there right now), AGENTS.md, data/captain.md, .agents/skills/flowy-collab/ (firstmate is editing those). If a fix seems to need one of those, STOP and report instead of editing. Any finding that proposes editing those files must be escalated, not applied.

REQUIREMENTS AS ACCEPTED:

  • Never print, echo or log the key, and avoid a file that outlives the call - avoid a file at all if possible.
  • Prefer ONE shared helper over repeating the pattern per script (explicitly requested).
  • Add a colocated test proving the key is absent from argv - a real behavioural check, NOT an assertion about source bytes.
  • Full lint gate green on both pinned linters (bin/fm-lint.sh: shellcheck + actionlint).
  • Do not add an agent name as a commit co-author. Do not merge.

WHAT WAS BUILT AND WHY: a single shared owner, fmx_curl_config, emits curl's url and header directives for 'curl -K -' to read on stdin. Callers build it with the printf BUILTIN and pipe it in, so the token never becomes any process's argv and never touches disk. Converted all three request sites: fmx_post_json (answer/followup/request-context), fm-x-poll.sh, fm-x-dismiss.sh. Values use curl's quoted-value syntax with backslash and double-quote escaped; a newline-bearing token is still refused outright (kept from the old helper) because the config is line-oriented.

DELIBERATE DESIGN DECISIONS a reviewer might otherwise flag:

  • The trailing newline in printf '%s\n' "$config" is load-bearing: command substitution strips it, and a final unterminated config line is fragile to parse.
  • The auth temp file and its EXIT/HUP/INT/TERM cleanup traps were deleted on purpose; there is no longer a file to clean up. That is the point of the change, not a dropped safeguard.
  • fmx_curl_config uses ${FMX_TOKEN-} so it is safe under set -u.
  • 'curl -K -' consuming stdin does not conflict with any call site: payloads are passed via --data-binary @file or --data, never stdin.
  • 'curl -K -' behaviour was verified empirically against a real local listener before building on it, including a token containing a double quote and a backslash.

TESTING APPROACH (deliberate): tests/fm-x-mode.test.sh's fake curl now records psargv - the command line it reads back FROM THE OS via ps - and the new tests assert the token is absent from it. This measures the actual exposure rather than restating the source, satisfying the explicit 'behavioural, not source bytes' requirement. Each case ALSO asserts the bearer still arrived in the auth header, so a client that simply stopped authenticating could not pass vacuously. This was mutation-tested: reintroducing the argv form makes the tests fail with the real leaked command line. An awkward-token case pins the quote/backslash escaping round trip. The pre-existing interrupted-post test previously asserted that a trap REMOVED the auth temp file; since no such file is created any more, it was rewritten to prove the stronger property - no credential file is written at all, checked against a private TMPDIR. That rewrite is intentional, not a weakened test.

KNOWN PRE-EXISTING FAILURES, NOT CAUSED BY THIS CHANGE: tests/fm-public-followup.test.sh ('could not register the public commitment') and tests/fm-gotmp.test.sh ('teardown did not remove the tasktmp dir') each fail one test. Both were baselined by stashing this change and re-running on the clean base, where they fail identically. They are broken on main and are out of scope for this task.

What Changed

  • Replaced fmx_auth_header_file with fmx_curl_config <url> in bin/fm-x-lib.sh: it emits url and header directives in curl's quoted-value syntax (escaping \ and ", still rejecting a token containing a newline or carriage return) and reads ${FMX_TOKEN-} so it is safe under set -u. The 0600 auth temp file and its EXIT/HUP/INT/TERM cleanup traps are gone — there is no longer a credential file to clean up.
  • Converted all three Relay request sites — fmx_post_json, bin/fm-x-poll.sh, and bin/fm-x-dismiss.sh — to pipe that config into curl -K - via builtin printf, dropping the -H "@file" argument and the inline URL argument. Payloads still travel via --data/--data-binary @file, so nothing else contends for stdin. docs/architecture.md now names fmx_curl_config as the single owner of how the token reaches curl.
  • tests/fm-x-mode.test.sh: the fake curl parses -K config from stdin or a file and records psargv, its own command line read back from the OS via ps. New poll/reply/dismiss cases assert the token is absent from that command line while the bearer still arrives in the auth header, plus a case pinning the quote/backslash escaping round trip. The interrupted-post test was rewritten to assert no credential file is written at all, checked against a private TMPDIR.

Risk Assessment

✅ Low: A well-bounded, single-purpose hardening of three curl call sites behind one shared helper: the escaping round-trips correctly against curl's actual config-value unescaping, exit-status and stdin semantics are unchanged (no pipefail, stdin consumed before the request), no stale references to the removed temp-file helper remain, the scope fence and commit-metadata constraints are honored, and both findings are minor robustness improvements rather than reachable defects.

Testing

Ran the colocated suite tests/fm-x-mode.test.sh (110 ok, 0 not ok), then went past unit-level evidence: I drove the shipped fm-x-poll.sh and fm-x-reply.sh with real curl against a real local HTTP listener while sampling ps from a separate process, and the bearer never appeared in curl's command line on either the GET or the POST path while the listener still received the correct Authorization: Bearer header and an intact JSON body. A control run of the briefed defective -H "Authorization: Bearer $tok" shape against the same listener did show the credential in ps, so the check is measuring the real exposure. A private TMPDIR watched throughout the call held no credential file, whereas the same harness against the base commit caught the old mode-0600 fm-x-auth.* file on disk — the concrete improvement this change makes. I also mutation-tested the new tests by reintroducing the argv form, which made them fail with the actual leaked command line, then reverted. The two test files the author flagged as broken on main fail identically after reverting this change's files to the base commit, so they are pre-existing and unrelated. No visual artifacts apply: this is a shell CLI and credential-handling change with no rendered surface. The transient harness was removed and the worktree is clean at the target commit.

Evidence: End-to-end: bearer absent from curl argv, present in the relay's auth header (real curl, real listener, real ps)

Source: End-to-end: bearer absent from curl argv, present in the relay's auth header (real curl, real listener, real ps)

=== 1. SHIPPED CLIENT: bin/fm-x-poll.sh against http://127.0.0.1 (a real listener) === curl processes sampled from the OS (ps -A -o comm=,args=) for the life of the call: curl -K - -m 5 -s -o /…/fm-x-poll.BD4hjY -w %{http_code} -H Accept: application/json grep -c "s3cr3t-relay-bearer-DO-NOT-LEAK" <ps sample> -> 0 => the bearer is NOT in curl argv. === 2. ...and the request still authenticated (relay-side view) === GET /connector/poll authorization: Bearer s3cr3t-relay-bearer-DO-NOT-LEAK === 3. CONTROL: the briefed defective shape, same listener, same sampler === curl -m 5 -s -o /dev/null -w control http_code=%{http_code}\n -H Authorization: Bearer s3cr3t-relay-bearer-DO-NOT-LEAK -H Accept: application/json http://127.0.0.1:60222/connector/poll&#10;=> ps DOES expose the credential for that shape. The measurement is real. === 4. Nothing on disk: private TMPDIR polled every 100ms for the whole call === files containing the token: (none) === 5. CONTRAST at base commit 822a990 (same harness) === -rw-------@ /…/tmp-base/fm-x-auth.al9NLA => the base client wrote a mode-0600 credential file to disk for the call. The target commit writes none at all. === 6. THE POST PATH: bin/fm-x-reply.sh -> POST /connector/answer (real socket) === $ fm-x-reply.sh req-e2e-001 "Aye captain - relay reply over a real socket." req-e2e-001 (exit 0) curl -K - -m 10 -s -o /…/fm-x-reply.CMTOcO -w %{http_code} -X POST -H Content-Type: application/json --data-binary @/…/fm-x-reply.FnBaiI token occurrences in that ps sample: 0 relay-side: POST /connector/answer authorization: Bearer s3cr3t-relay-bearer-DO-NOT-LEAK body: {"request_id":"req-e2e-001","text":"Aye captain - relay reply over a real socket."} => bearer absent from argv, bearer present in the header, JSON body intact - 'curl -K -' reading stdin does not contend with --data-binary @file.

Real curl, real local HTTP relay, real ps. Token used: s3cr3t-relay-bearer-DO-NOT-LEAK

=== 1. SHIPPED CLIENT: bin/fm-x-poll.sh against http://127.0.0.1 (a real listener) ===
curl processes sampled from the OS (ps -A -o comm=,args=) for the life of the call,
exactly what any other local process could read:

    curl -K - -m 5 -s -o /Users/cedmo/.no-mistakes/worktrees/263c604673df/01M0R1DSK5XVBR6DAMZ1Y3EGDY/tmp/fmx-e2e/tmp/fm-x-poll.BD4hjY -w %{http_code} -H Accept: application/json

    grep -c "s3cr3t-relay-bearer-DO-NOT-LEAK" <ps sample>  ->  0
    => the bearer is NOT in curl argv.

=== 2. ...and the request still authenticated (relay-side view) ===
    GET /connector/poll
    authorization: Bearer s3cr3t-relay-bearer-DO-NOT-LEAK
    GET /connector/poll
    authorization: Bearer s3cr3t-relay-bearer-DO-NOT-LEAK
    (two GETs: the shipped client, then the control below)

=== 3. CONTROL: the briefed defective shape, same listener, same sampler ===
    curl -m 5 ... -H "Authorization: Bearer $TOKEN" http://127.0.0.1:PORT/connector/poll

    curl -m 5 -s -o /dev/null -w control http_code=%{http_code}\n -H Authorization: Bearer s3cr3t-relay-bearer-DO-NOT-LEAK -H Accept: application/json http://127.0.0.1:60222/connector/poll
    => ps DOES expose the credential for that shape. The measurement is real;
       the shipped client simply does not produce such a line.

=== 4. Nothing on disk: private TMPDIR polled every 100ms for the whole call ===
    files containing the token: (none)
    TMPDIR after the run:
    total 0
    drwxr-xr-x@  2 cedmo  staff   64 Aug 23 21:36 .
    drwxr-xr-x@ 11 cedmo  staff  352 Aug 23 21:37 ..

=== 5. CONTRAST at base commit 822a990 (same harness, base bin/fm-x-{lib,poll}.sh) ===
    -rw-------@  /Users/cedmo/.no-mistakes/worktrees/263c604673df/01M0R1DSK5XVBR6DAMZ1Y3EGDY/tmp/fmx-e2e/tmp-base/fm-x-auth.al9NLA
    => the base client wrote a mode-0600 credential file to disk for the call.
       The target commit writes none at all.

=== 6. THE POST PATH: bin/fm-x-reply.sh -> POST /connector/answer (real socket) ===
    $ fm-x-reply.sh req-e2e-001 "Aye captain - relay reply over a real socket."
    req-e2e-001            (exit 0)

    curl process as ps sees it:
      curl -K - -m 10 -s -o /Users/cedmo/.no-mistakes/worktrees/263c604673df/01M0R1DSK5XVBR6DAMZ1Y3EGDY/tmp/fmx-e2e/tmp-r/fm-x-reply.CMTOcO -w %{http_code} -X POST -H Content-Type: application/json --data-binary @/Users/cedmo/.no-mistakes/worktrees/263c604673df/01M0R1DSK5XVBR6DAMZ1Y3EGDY/tmp/fmx-e2e/tmp-r/fm-x-reply.FnBaiI
      token occurrences in that ps sample: 0

    relay-side:
      POST /connector/answer
      authorization: Bearer s3cr3t-relay-bearer-DO-NOT-LEAK
      body: {"request_id":"req-e2e-001","text":"Aye captain - relay reply over a real socket."}
      

    => bearer absent from argv, bearer present in the header, and the JSON body
       arrived intact - so 'curl -K -' reading stdin does not contend with
       --data-binary @file. No credential file appeared under the private TMPDIR.
Evidence: Mutation check + pre-existing-failure baseline

Source: Mutation check + pre-existing-failure baseline

MUTATION CHECK - do the new tests actually catch the leak? Reintroduced -H &#34;Authorization: Bearer $FMX_TOKEN&#34; in bin/fm-x-poll.sh, re-ran the suite: not ok - poll leaked the bearer token into curl argv (visible via ps): psargv=bash /var/.../argv-poll/fakebin/curl -m 5 -s -o /var/.../fm-x-poll.fGk3d4 -w %{http_code} -H Authorization: Bearer tok-argv-poll -H Accept: application/json https://relay.test/connector/poll&#10;&#10;The failure carries the REAL leaked command line read back from the OS, not a source-text assertion. Mutation reverted; worktree restored to the target commit. TARGETED SUITE AT THE TARGET COMMIT: 110 ok, 0 not ok ok - fm-x-poll keeps the bearer token out of curl argv ok - fm-x-reply keeps the bearer token out of curl argv ok - fm-x-dismiss keeps the bearer token out of curl argv ok - the curl stdin config escapes quotes and backslashes in a token ok - fm-x-reply writes no credential temp file, even on an interrupted post PRE-EXISTING FAILURES, VERIFIED NOT CAUSED BY THIS CHANGE (the four changed files reverted to 822a990, same two test files re-run) tests/fm-gotmp.test.sh target: not ok - teardown did not remove the tasktmp dir base: not ok - teardown did not remove the tasktmp dir tests/fm-public-followup.test.sh target: PF_REGISTRY_LOCK_IDS[@]: unbound variable / not ok - could not register the public commitment base: PF_REGISTRY_LOCK_IDS[@]: unbound variable / not ok - could not register the public commitment

MUTATION CHECK - do the new tests actually catch the leak?

Reintroduced the briefed defective shape in bin/fm-x-poll.sh:
    code=$(curl -m 5 -s -o "$BODY_FILE" -w %{http_code} \
      -H "Authorization: Bearer $FMX_TOKEN" \
      -H "Accept: application/json" "$FMX_RELAY/connector/poll" ...)

then re-ran bash tests/fm-x-mode.test.sh:

    not ok - poll leaked the bearer token into curl argv (visible via ps): psargv=bash
    /var/.../argv-poll/fakebin/curl -m 5 -s -o /var/.../fm-x-poll.fGk3d4 -w %{http_code}
    -H Authorization: Bearer tok-argv-poll -H Accept: application/json
    https://relay.test/connector/poll

The failure message carries the REAL leaked command line read back from the OS,
not a source-text assertion. Mutation reverted; worktree restored to the target commit.

==================================================================
TARGETED SUITE AT THE TARGET COMMIT: bash tests/fm-x-mode.test.sh
  110 ok, 0 not ok. The cases that pin this change:

    ok - fm-x-poll keeps the bearer token out of curl argv
    ok - fm-x-reply keeps the bearer token out of curl argv
    ok - fm-x-dismiss keeps the bearer token out of curl argv
    ok - the curl stdin config escapes quotes and backslashes in a token
    ok - fm-x-reply writes no credential temp file, even on an interrupted post
    ok - fm-x-reply streams large image payloads outside curl argv

==================================================================
PRE-EXISTING FAILURES, VERIFIED NOT CAUSED BY THIS CHANGE
(bin/fm-x-{lib,poll,dismiss}.sh + tests/fm-x-mode.test.sh reverted to 822a990,
 same two files re-run -> identical failures)

    tests/fm-gotmp.test.sh
      target: not ok - teardown did not remove the tasktmp dir
      base:   not ok - teardown did not remove the tasktmp dir
    tests/fm-public-followup.test.sh
      target: bin/fm-public-followup.sh: line 142: PF_REGISTRY_LOCK_IDS[@]: unbound variable
              not ok - could not register the public commitment
      base:   bin/fm-public-followup.sh: line 142: PF_REGISTRY_LOCK_IDS[@]: unbound variable
              not ok - could not register the public commitment

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ bin/fm-x-lib.sh:758 - fmx_curl_config rejects a newline/CR in the token (line 753) but applies no such guard to the URL, which is emitted into the same line-oriented curl -K - config. A newline in FMX_RELAY_URL splits the config into extra lines that curl parses as additional directives (e.g. output = ..., proxy = ..., a second header = ...). Reachability is limited: the .env path (fmx_env_get, bin/fm-x-lib.sh:56) is line-based and cannot yield a newline, and &#34; is already escaped, so it requires an operator-set env var containing a literal newline - not a privilege-boundary crossing. Since this function is documented as the single owner of how the request reaches curl, extend the existing case &#34;$token&#34; in *$&#39;\n&#39;*|*$&#39;\r&#39;*) return 1 guard to cover $url as well.
  • ℹ️ tests/fm-x-mode.test.sh:2905 - assert_token_off_argv takes tail -1 of the ^psargv= lines, so the OS-derived command line - the evidence the intent designates as the real behavioural check - is inspected for only the final relay call, while the weaker stub-reconstructed ^argv= check (line 2910) scans every call. This is currently harmless: all four new cases issue exactly one relay call, because fmx_resolve_reply_context only reaches fmx_request_relay_context when allow_relay=1, which requires --followup, and none of these tests pass it. But the helper reads as general-purpose, so a follow-up-path case added later (request-context POST followed by the followup POST) would have its first call covered only by the $*-reconstructed string. Drop the tail -1 and assert that no ^psargv= line contains the token.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-x-mode.test.sh — 110 ok, 0 not ok, including fm-x-poll/fm-x-reply/fm-x-dismiss keeps the bearer token out of curl argv, the curl stdin config escapes quotes and backslashes in a token, and fm-x-reply writes no credential temp file, even on an interrupted post
  • Mutation check: reintroduced -H &#34;Authorization: Bearer $FMX_TOKEN&#34; in bin/fm-x-poll.sh, re-ran bash tests/fm-x-mode.test.shnot ok - poll leaked the bearer token into curl argv (visible via ps) carrying the real leaked command line; mutation reverted via git checkout -- bin/fm-x-poll.sh
  • Manual E2E (GET path): ran the shipped bin/fm-x-poll.sh with real curl against a python3 HTTP listener on 127.0.0.1 that holds the response 2s, while sampling ps -A -o comm=,args= every 100ms — sampled argv was curl -K - -m 5 -s -o … -w %{http_code} -H Accept: application/json, zero token occurrences, and the listener received Authorization: Bearer &lt;token&gt;
  • Manual E2E (POST path): ran bin/fm-x-reply.sh req-e2e-001 &#34;…&#34; against the same listener — argv contained curl -K - … -X POST … --data-binary @file with zero token occurrences, listener received the bearer header and the intact JSON body (proves -K - stdin does not contend with the payload file)
  • Control run: curl -m 5 -s -o /dev/null -w &#39;%{http_code}&#39; -H &#34;Authorization: Bearer $TOKEN&#34; http://127.0.0.1:$PORT/connector/poll under the same sampler — ps DOES expose the full credential, confirming the measurement is real
  • On-disk check: polled a private TMPDIR with grep -rl &#34;$TOKEN&#34; every 100ms for the whole call — no credential file at any point, and the directory was empty afterward
  • Base contrast: ran base-commit 822a990 copies of bin/fm-x-{lib,poll}.sh under the same harness — a -rw------- fm-x-auth.* credential file appeared on disk during the call
  • Baseline of known failures: bash tests/fm-gotmp.test.sh and bash tests/fm-public-followup.test.sh at the target commit, then again after reverting bin/fm-x-{lib,poll,dismiss}.sh and tests/fm-x-mode.test.sh to 822a990 — identical single-test failures both times
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

cedmos added 2 commits August 23, 2026 21:26
Process arguments are world readable via ps for the life of a call, so a
bearer passed as `-H "Authorization: Bearer $tok"` exposes the credential
to any local process. The fm-x-* relay clients did not do that - they
already routed the header through a 0600 temp file - but that file is
still a credential on disk kept alive by a cleanup trap, and a SIGKILL
skips traps.

Replace the temp file with a single shared owner, fmx_curl_config, that
emits curl's url and header directives for `curl -K -` to read on stdin.
The config is built with the printf builtin and piped in, so the token
never becomes any process's argv and never touches disk at all. Values
use curl's quoted-value syntax with backslash and double quote escaped;
a token containing a newline is still refused outright.

Converts all three request sites: fmx_post_json (answer, followup,
request-context), fm-x-poll.sh, and fm-x-dismiss.sh.

Tests assert the guarantee behaviourally: the fake curl records the
command line it reads back from the OS, and each case additionally
asserts the bearer still arrived in the auth header so a client that
stopped authenticating could not pass. Reintroducing the argv form makes
them fail. The interrupted-post test now proves the stronger property -
no credential file is written at all - instead of that a trap removed it.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge, with no unaddressed blocking or independently publishable findings.

The converted request paths preserve curl failure handling and keep request bodies separate from configuration stdin, while the remaining URL-validation and multi-call test concerns have already been communicated in the existing review.

Reviews (1): Last reviewed commit: "no-mistakes(document): document Relay be..." | Re-trigger Greptile

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.

1 participant