scripts/publish-deb.sh — keep the release token out of curl's argv, the contract publish-release.sh already keeps one step later #62
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:ci
scope:cli
scope:docs
scope:manifests
scope:packaging
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/stoke#62
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Context
.forgejo/workflows/release.yml'sdebjob runs two publishing steps back to back, on one runner, with one secret. They disagree about how to hand that secret to a child process, and the disagreement is not an accident — one of them was written to a spec that forbids what the other does.The step that gets it right. #54 specified
scripts/publish-release.shand decided the handling in its own words:That shipped in !55.
publish-release.shL19-31, L41 is exactly that:mktemp -dunderRUNNER_TEMP,trap 'rm -rf "$TMP"' EXIT,chmod 0600on the token file,--token-file. Its test even names the valuerelease-token-that-must-not-enter-argvand asserts on the recordedargv.The step immediately before it does the forbidden thing.
scripts/publish-deb.shL46-48:The shell expands
$TOKENbeforeexec, so the literal secret is an element of curl'sargvfor the whole upload.Why
argvand not just "it is in the environment anyway". Both scripts receive the secret through the step'senv:block, and that is not the complaint. The two are not equally exposed:/proc/<pid>/environ0400/proc/<pid>/cmdline0444(Measured on Linux 6.12:
stat -c '%a' /proc/self/environ /proc/self/cmdline→400,444.) Putting the token inargvwidens a secret that was owner-only to world-readable, for the duration of a.debupload. That is the whole delta, and it is the one #54 named.The blast radius today is one job's container (
container: node:22-bookworm), which is modest — and it is not a property this repository controls.release.yml's own header invites the operator to "Adjustruns-onto a label your runner actually advertises"; a runner without per-job container isolation makes the same line a cross-tenant leak with no change here.Provenance — a five-day-old audit finding, filed today
This is finding 1 of the six in the closed !21, re-verified against
mainat2230ca25today. Triage asked @andres on 2026-08-30 (!21 comment 28090) whether that close was hygiene or a verdict on the findings, promising to mint on "hygiene". No answer came, and both threads that carried the question were closed — so the question had no open home to be answered in. Triage has decided it as hygiene and is accountable for that call; closing this issue overturns it in one click.Not a duplicate. #57 rewrote this file's empty-token message and added its test; it named the
/tmpresponse file an explicit non-goal ("a real wart, not this one") and never touchedargv. #60 documentedRELEASE_TOKEN's scope set. Neither changed how the token reachescurl.Spec — decisions
Pass the header through a file, not
argv.curl -H @<file>reads the header line from a file; the secret never becomes an argument. Verified live against this forge on curl 8.14.1 —-H @filewith a bad token returns401(the header was sent and rejected) and with the real token returns200. The form has existed since curl 7.55;node:22-bookwormships 7.88. No workflow change and no new dependency.One temp directory, trap-cleaned, mirroring the sibling. Create it the way
publish-release.shL19-24 does —mktemp -d "$RUNNER_TEMP/stoke-publish.XXXXXX"whenRUNNER_TEMPis set, plainmktemp -dotherwise — withtrap 'rm -rf "$TMP"' EXITinstalled before anything is written into it. The header file goes there at mode0600. The response body file moves there too and the two hand-rolledrm -fcalls at L56/L60 come out: the trap covers every exit path, including the signal paths they never covered.Everything else is unchanged and that is a requirement, not an accident. The two-source token resolution at L29-31, the L33-39 message #57 shipped,
--upload-file, the201/409/other status handling at L50-59, the stderr dump of the response body on failure, andexit 1all stay exactly as they are.Out of scope
--max-time. The file has none, and adding one means choosing a number that must not kill a legitimately slow upload of a real.debover a real link. Nobody here can justify a value; a wrong one turns a working release red. If it is wanted, it is its own issue with its own reasoning.publish-release.sh. It is the exemplar here, not the patient..forgejo/workflows/release.yml. The step keeps passingSTOKE_TOKENinenv:; that interface does not change.Tasks
scripts/publish-deb.sh, created after the L33 token guard and before the upload.Authorization: token $TOKENinto a0600file in that directory and replace the inline-Hwith-H @"$HEADER_FILE".rm -f /tmp/stoke-publish-response.$$lines.test/publish-deb.test.jswith theargvand mode assertions below, using a PATH-stubbedcurlthat records its arguments — the stub pattern istest/publish-release.test.js.changelog.d/fragment.Refs, and let triage close this issue; all criteria here are pre-merge, so aClosesis equally correct — just do not leave it open with everything ticked.Acceptance criteria
curlonPATHthat appends its fullargvto a log, a run withSTOKE_TOKEN=<sentinel>produces a log in which the sentinel appears zero times. Name the sentinel so a reader of a failure knows what it means, aspublish-release.test.jsdoes.-H @<path>, where<path>is inside the script's temp directory, contains exactlyAuthorization: token <sentinel>, and has mode0600.201path, on the failure path, and when the stub exits non-zero — the temp directory no longer exists, and no/tmp/stoke-publish-response.*file is left behind.409and201messages and the failure path's stderr dump of the response body are byte-identical to today's, proven by a test per status.STOKE_TOKENstill gets past the L33 guard, and an empty one still exits 1 with #57's message — the guard was not disturbed.git difftouches exactlyscripts/publish-deb.sh,test/publish-deb.test.jsand onechangelog.d/*.md.npm testpasses andci / testis green on the PR head.Test plan
npm test, no network: the stubcurlnever reaches the registry. The cases that must fail:$TOKENproves nothing — the question is what reachesargvat runtime, so the assertion must read the stub's recorded arguments.0600, fails the criterion even if the upload succeeded.Dependencies
No blockers, and this deliberately declares no collision edge: it is the only open issue naming
scripts/publish-deb.sh, and it shares no file with the other issues minted from !21 this tick.Related: #57 and #60 (this file and its workflow, both merged), #54 (the decision this issue applies), and the closed !21, where the finding was first recorded.
Starting work on #62 as codex-bot-andresmgsl.
Design / plan of record:
publish-debruntime harness, stubbing only externalcurl, to capture argv plus the referenced header file contents, mode, and temp-directory path before cleanup.0600authorization header and response body there; invoke curl with-H @<header-file>.This is a bounded implementation of the decided triage spec; no additional design choices are being introduced.