Skip to content

fix: keep the GitHub token off curl argv in the gh fallback - #1769

Merged
mrbobbytables merged 1 commit into
mainfrom
sec/pr-queue-hygiene-token-argv
Oct 22, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
sec/pr-queue-hygiene-token-argv

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

scripts/pr-queue-hygiene.mjs falls back to curl on runner images that carry
no gh binary, and passed the credential as an argv element:

if (token) argv.push('-H', `Authorization: Bearer ${token}`);

Two exposures follow from that, both verified rather than assumed:

  • argv is world-readable through /proc/<pid>/cmdline for as long as the
    call runs.
  • Node builds execFileSync's Command failed: ... error message out of the
    same argv
    , and -sf makes curl exit non-zero on any 4xx/5xx — so one 403
    produced an error carrying the token. Only ENOENT was caught in
    ghViaCurl(); everything else is rethrown, so that message reached the log.

The token is ${{ github.token }} from .github/workflows/pr-queue-hygiene.yml
with pull-requests: write and issues: write.

What changes

  • The header goes to curl on stdin via --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 parser
    reads a quoted value with backslash escapes; an unescaped quote in a
    credential would end the header early.
  • The invocation is built by an exported curlInvocation(), so tests can
    assert 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.mjs now logs each stub invocation's stdin next to
its argv, and exposes it as stdins. The existing test fallback: main removes the stale label over REST when gh is missing asserted the Authorization header
appeared 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
--config and no stdin when there is no token, and escaping of a quote or
backslash in the token.

Scope

Touches only scripts/pr-queue-hygiene.mjs, tests/pr-queue-hygiene.test.mjs
and tests/helpers-gh-sandbox.mjs. Disjoint from the other open hold-gated
PRs: #1763 (scripts/audit-gate.mjs), #1767 (validator scripts), #1761
(scripts/lib/url-safety.mjs tests), #1765 (SVG allowlist tests).

Verification

npm run test:unit 3029/3029 pass, and check:format, check:lint,
check:duplication, check:unused are 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

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>
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

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 outreach PR that targets a repository this hive does not manage is held at every level because it publishes project-facing communication into someone else's project.

Hive will keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

@netlify

netlify Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for endusers ready!

Name Link
🔨 Latest commit efdbb34
🔍 Latest deploy log https://app.netlify.com/projects/endusers/deploys/6ad9c332fcdcc47537e20963
😎 Deploy Preview https://deploy-preview-1769--endusers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 one header = "Authorization: Bearer <token>" line ending in "\n; with no token there is no --config and 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 intercepts ENOENT; every other execFileSync error — 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. Ran node --test tests/pr-queue-hygiene.test.mjs at 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

@mrbobbytables

Copy link
Copy Markdown
Member

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.

@mrbobbytables
mrbobbytables added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 53c6df8 Oct 22, 2026
14 checks passed
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.

[sec-check] pr-queue-hygiene's curl fallback puts the GitHub token on argv, where /proc and execFileSync's error message both expose it

1 participant