Harden release.yml sed version substitutions against metacharacter breakage - #261
Merged
mikebronner merged 4 commits intoJul 14, 2026
Conversation
Validate $VERSION against a strict semver regex immediately after extraction in release.yml and fail the step on mismatch, before anything is written to $GITHUB_OUTPUT. The three sed version substitutions, the version-bump commit message, and the tag/release curl calls all consume this value (or the tag it derives from), so the single up-front guard keeps sed metacharacters (/, &, \) and quote-breakers out of every downstream call-site. Fixes: #243
mikebronner
marked this pull request as ready for review
July 14, 2026 18:27
Clippy 1.97 (current stable on CI runners) flags both match-on-Option blocks under the question_mark lint, and CI runs clippy with -D warnings, so main is red on every PR. Behavior is unchanged — both sites collapse to the equivalent ? expression.
Third site clippy 1.97 flags under the question_mark lint (this one only surfaced once the first two stopped aborting compilation). Verified clean locally on clippy 1.97 with --all-targets -D warnings — the same invocation CI runs. Behavior unchanged; all 2553 tests pass.
There was a problem hiding this comment.
✅ Approved
Review Summary
Reviewed PR #261 against #243's five acceptance criteria — fanned out AC-conformance, correctness, security, and test-honesty lenses over the checkout, then adversarially verified the one in-PR security candidate.
- All 5 AC met. The single up-front guard (
release.yml:51-55) uses the AC's exact regex^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$, placed immediately afterVERSION="${TAG#v}", with::error::+exit 1on mismatch. All threesedsites (Cargo.toml,extension.toml,laravel-lsp/Cargo.toml), the commit message, and the curl calls inherit a validated value — AC #4's "single guard, all call-sites inherit" strategy, exactly as specified. AC #1–#3 are met by the validation branch the AC explicitly permits ("strict semver validation and/or escaped replacement"). AC #5 confirmed by a full-file scan: no residual unescaped tag-derived value reaches a shell tool. - Guard verified against the threat: it rejects
/,&,\,;, spaces, and newline payloads (anchored^…$, charset[0-9A-Za-z.-]) while accepting every existing and legitimate tag (0.1.0…0.6.0,1.2.3-beta.1). A crafted tag can no longer break thesedsubstitutions. - CI green — clippy
-D warnings, LSP tests, extension wasm check all pass.
What's Good
- Chose the AC's preferred single-guard approach: one validation covers all five downstream consumers instead of re-delimiting/escaping each
sedsite independently. Correct, minimal, and maintainable. - The three Rust
?-operator refactors (livewire_resolver.rs:210,main.rs:8166,query_chain/cursor.rs:396) are mechanical clippyquestion_markfixes and are appropriate to include here: the floatingdtolnay/rust-toolchain@stablesurfaced pre-existing lints that-D warningsnow fails on, so this PR's CI cannot go green without them. I verified each is an exact behavioral equivalent — same return type, same early-return-Nonecases, same clone semantics, and inmain.rsthe loop body and trailingif !found_in_previous { return None; }moved correctly out of theif let.
📋 Non-blocking follow-ups
- Curl step re-derives the raw tag instead of the validated output —
release.yml:136-161. The "Update tag and release" curl interpolates raw${TAG_NAME}(github.event.release.tag_name), not the guard-validatedsteps.extract_version.outputs.version. Safe today (the guard runs earlier in the same job andexit 1aborts before this step, and for a release eventTAG_NAMEis the same value the guard validated), but the safety rides on same-job ordering rather than on consuming the validated value — a future reorder or job-split would silently re-open it. Routing curl through the validated output makes the guarantee structural. Outside this PR's changed lines and beyond AC #5 (which is met by confirmation), so non-blocking. - Guard's error path echoes the raw tag —
release.yml:53.echo "::error::Tag '${TAG}' …"prints the pre-strip raw tag; a newline in aworkflow_dispatchtag input could inject an extra annotation-class workflow command. Adversarially verified as not a blocker: env-var indirection blocks any shell/sedinjection, only non-executing annotation commands survive (the RCE-capableset-env/add-path/set-outputwere disabled in 2020),::stop-commands::is inert becauseexit 1follows immediately, and exploitation requires an already-write-privileged actor. Cosmetic log-hygiene — worth neutralizing while touching this file, but not a merge gate.
(FYI, no action needed: the regex is marginally looser than "strict semver" — it accepts 1.2.3.4 and 01.2.3 — but that is the AC's own verbatim pattern, and both forms contain only [0-9.], so metacharacter-safety is fully preserved. Not a defect against this PR.)
Both follow-ups are the same theme (residual tag-derived-value handling on non-critical release.yml paths) and are being tracked as one defense-in-depth issue.
Ready for @mikebronner to merge.
Closed
2 tasks
mikebronner
deleted the
chore/243-harden-releaseyml-sed-version-substitutions-agains
branch
July 14, 2026 19:46
mikebronner
added a commit
that referenced
this pull request
Jul 15, 2026
Two defense-in-depth tightenings from Holmes's review of #243 (PR #261), turning "safe-by-ordering" into "safe-by-construction": - The "Update tag and release" curl step now consumes the guard-validated steps.extract_version.outputs.tag instead of the raw github.event.release.tag_name, so the URL/JSON interpolations stay hardened even if steps are reordered or split across jobs. The guard step exports the tag as an output only after the semver check passes (TAG is structurally VERSION or v${VERSION} at that point). - The guard's ::error:: echo strips CR/LF from the raw tag/version before interpolation, so a newline in a workflow_dispatch tag input can't split the annotation and forge an additional workflow command. No change to the guard's pass/fail logic, the sed substitutions, or the commit step. Fixes #262
Merged
7 tasks done
mikebronner
added a commit
that referenced
this pull request
Jul 15, 2026
…dated version output (#268) * chore: start work on #262 * ci: 🔒️ harden residual tag-derived values in release.yml Two defense-in-depth tightenings from Holmes's review of #243 (PR #261), turning "safe-by-ordering" into "safe-by-construction": - The "Update tag and release" curl step now consumes the guard-validated steps.extract_version.outputs.tag instead of the raw github.event.release.tag_name, so the URL/JSON interpolations stay hardened even if steps are reordered or split across jobs. The guard step exports the tag as an output only after the semver check passes (TAG is structurally VERSION or v${VERSION} at that point). - The guard's ::error:: echo strips CR/LF from the raw tag/version before interpolation, so a newline in a workflow_dispatch tag input can't split the annotation and forge an additional workflow command. No change to the guard's pass/fail logic, the sed substitutions, or the commit step. Fixes #262
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #243 — defense-in-depth follow-up from the review of #240 (PR #242). The "Update versions and lock files" step embedded
$VERSION(tag-derived) unescaped into threesedsubstitution expressions; a tag containingsedmetacharacters (/,&,\) would break them.Changes
extract_versionstep of.github/workflows/release.yml, immediately afterVERSION="${TAG#v}"and before the$GITHUB_OUTPUTwrite (so a hostileworkflow_dispatchtag can't inject step outputs either).^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$— permits only digits, dots, and[-.][0-9A-Za-z.-]suffixes, so nosedmetacharacter, quote, space, or newline can pass.sedcall-sites inherit the guarantee from the single guard — no per-site escaping needed.sedversion substitutions against metacharacter breakage #243's subject but required for a green gate): clippy 1.97 (current stable on CI runners) newly flags threeclippy::question_marksites onmain(livewire_resolver.rs:210,query_chain/cursor.rs:393,main.rs:8166), and CI runsclippy --all-targets -- -D warnings, so every PR was red. Replaced the three match/if-let blocks with the equivalent?expressions — behavior unchanged, two atomicstylecommits.Acceptance Criteria
release.yml—Cargo.tomlsed: $VERSION substitution safe (strict semver validation up front)release.yml—extension.tomlsed: same hardeningrelease.yml—laravel-lsp/Cargo.tomlsed: same hardening^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$immediately afterVERSION="${TAG#v}", step fails on mismatch, all three call-sites inherit the guaranteeTAG_NAMEin the "Update tag and release"curlcalls runs in the same job after the guard step (guard failure kills the job first), and a guardedVERSIONconstrains the tag itself tov?+[0-9A-Za-z.-]— no/,&,\, or"can reach the URL or JSON body.matrix.*/runner.osare workflow constants; thesoftpropstag_name:is an action input, not shell interpolation.Test Plan
[[ =~ ]]semantics as the runner): accepts1.2.3,0.6.0,1.2.3-beta.1,1.2.3.4,10.20.30-rc-2; rejects1.2,v1.2.3,1.2.3/x,1.2.3&,1.2.3\1e,1.2.3"; curl evil, trailing space, empty string, newline-injection payloadbash -e: benign tags (v0.6.1,v1.2.3-beta.1) → rc=0,version=output written; hostile tags (v1.2.3/&\,v1.2.3"; curl evil) → rc=1,::error::emitted, nothing written to$GITHUB_OUTPUTYAML.load_file)--all-targets -- -D warnings(same invocation as CI);cargo fmt --checkclean; all 2553 LSP tests passFixes #243