Repository navigation
fix: keep the GitHub token off curl argv in the gh fallback - #1769
Conversation
scripts/pr-queue-hygiene.mjs falls back to curl on runner images with no gh
binary, and passed the credential as an argv element:
argv.push('-H', `Authorization: Bearer ${token}`)
argv is world-readable through /proc/<pid>/cmdline for the life of the call,
and Node builds execFileSync's "Command failed: ..." error message out of the
same argv -- with -sf, which makes curl exit non-zero on any 4xx/5xx, a single
403 produced an error carrying the token. Only ENOENT was caught, so that
message reached the job log.
Hand the header to curl on stdin with `--config -` instead: not argv, and not
a file on disk either. curl's config parser reads a quoted value with
backslash escapes, so the value is escaped rather than relying on GitHub
tokens never containing a quote or backslash.
The invocation is built by an exported curlInvocation(), so the tests can
assert the token's absence from argv directly instead of inferring it from a
call that would have to reach the network.
The gh/curl sandbox stub now logs stdin alongside argv, because stdin is where
a credential is supposed to be and a test proving the token was sent has
nowhere else to look. The existing REST-fallback test asserted the header
appeared in argv, which is the behaviour removed here; its intent is kept by
moving the evidence to stdin and adding the negative assertion that the token
is absent from argv.
Closes #1768
Signed-off-by: sec-check <280983584+hivecommons-hive[bot]@users.noreply.github.com>
|
Important Held for human review by the hive's ACMM level gate. This PR was opened by the "sec-check" agent while Hive policy required a human checkpoint for that agent. Agent PRs are held at ACMM L3–L5; an Hive will keep the |
✅ Deploy Preview for endusers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Verified correct — this fixes the token exposure it describes, and the tests got stronger, not weaker.
Read at head efdbb34, not just the diff:
scripts/pr-queue-hygiene.mjs:174-193(curlInvocation): I executed the function directly rather than trusting the diff (my read path masks credential-shaped literals). Confirmed: with a token, argv begins--config -and the token appears nowhere in argv; stdin is exactly oneheader = "Authorization: Bearer <token>"line ending in"\n; with no token there is no--configand stdin is empty.escapeCurlConfigValue(scripts/pr-queue-hygiene.mjs:155-157) correctly escapes\then", in that order, which is the right order (escaping"first would double-escape the inserted backslashes). curl's quoted config-value parser honors both.- The rethrow path the PR body describes is real:
ghViaCurl's catch (scripts/pr-queue-hygiene.mjs:204-212) only interceptsENOENT; every otherexecFileSyncerror — whose message Node builds from argv — propagates. Moving the header off argv closes that leak. tests/pr-queue-hygiene.test.mjs+tests/helpers-gh-sandbox.mjs: the stub now logs stdin per invocation and the fallback test asserts the header is present on stdin and absent from argv — strictly stronger than the old argv-presence assertion. Rannode --test tests/pr-queue-hygiene.test.mjsat head: 0 failures.
One known, acceptable residue (also acknowledged in the code comment): a token containing a newline would truncate the config line — GitHub tokens cannot contain one, and the escape function covers the two characters curl's quoted parser actually interprets.
This looks correct to me. Hold-gated per policy, so a maintainer still needs to sign off, but I found nothing blocking.
— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.88
|
Maintainer-delegated P1 merge decision: the maintainer explicitly authorized this specific PR, its hold release, and normal protected merge-queue submission under the documented sole-maintainer path. Rationale: the bounded change removes credentials from curl argv while retaining authorization on stdin; focused validation at efdbb34 passes, existing independent agent review found no blocker, and current required checks are green. An agent review is not being represented as independent human approval. No protection or required check will be bypassed. |
Security Fix
scripts/pr-queue-hygiene.mjsfalls back tocurlon runner images that carryno
ghbinary, and passed the credential as an argv element:Two exposures follow from that, both verified rather than assumed:
/proc/<pid>/cmdlinefor as long as thecall runs.
execFileSync'sCommand failed: ...error message out of thesame argv, and
-sfmakes curl exit non-zero on any 4xx/5xx — so one 403produced an error carrying the token. Only
ENOENTwas caught inghViaCurl(); everything else is rethrown, so that message reached the log.The token is
${{ github.token }}from.github/workflows/pr-queue-hygiene.ymlwith
pull-requests: writeandissues: write.What changes
--config -instead of on argv —neither argv nor a file on disk. Verified in both directions: the thrown
error no longer contains the value, and the header still arrives.
escapeCurlConfigValue()escapes\and", because curl's config parserreads a quoted value with backslash escapes; an unescaped quote in a
credential would end the header early.
curlInvocation(), so tests canassert the token's absence from argv directly rather than inferring it from
a call that would have to reach the network.
Test changes worth a reviewer's eye
tests/helpers-gh-sandbox.mjsnow logs each stub invocation's stdin next toits argv, and exposes it as
stdins. The existing testfallback: main removes the stale label over REST when gh is missingasserted the Authorization headerappeared in argv — exactly the behaviour removed here. Its intent (prove the
token really is sent) is preserved by moving the evidence to stdin and adding
the matching negative assertion that the token does not appear in argv, so the
test is strictly stronger than before rather than relaxed.
Three new tests: token on stdin and never in argv (GET and POST shapes), no
--configand no stdin when there is no token, and escaping of a quote orbackslash in the token.
Scope
Touches only
scripts/pr-queue-hygiene.mjs,tests/pr-queue-hygiene.test.mjsand
tests/helpers-gh-sandbox.mjs. Disjoint from the other open hold-gatedPRs: #1763 (
scripts/audit-gate.mjs), #1767 (validator scripts), #1761(
scripts/lib/url-safety.mjstests), #1765 (SVG allowlist tests).Verification
npm run test:unit3029/3029 pass, andcheck:format,check:lint,check:duplication,check:unusedare all green.Closes #1768
Filed by sec-check agent (ACMM L4/L5 — hold-gated mode). Hold-gated: human review required.
— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88