fix(scripts): pin self-update + SPM dependency to immutable revision (DEVA11Y-475,478,477)#30
fix(scripts): pin self-update + SPM dependency to immutable revision (DEVA11Y-475,478,477)#30Crash0v3rrid3 wants to merge 2 commits into
Conversation
…(DEVA11Y-475,478,477)
Supply-chain / integrity hardening for the shell installers (APPSEC-415).
DEVA11Y-475 / F-003 + DEVA11Y-478 / F-006 — self-update from mutable branch HEAD:
- Self-update is now OPT-IN via an explicit `--self-update` subcommand; it no
longer runs automatically on every invocation.
- Fetches from a pinned, immutable revision instead of refs/heads/main.
- Verifies a SHA-256 checksum (published `.sha256` sidecar) before use and
refuses to apply on mismatch or missing checksum.
- Downloads to a temp file and atomically replaces the script via `mv` instead
of overwriting the currently-running file in place.
- Applied consistently to all six variants (bash/zsh/fish × cli.sh/spm.sh).
DEVA11Y-477 / F-005 — SPM dependency pinned to branch "main":
- Generated Package.swift heredoc now pins to
.revision("db817c37cf74cba47e2fef535f53a35bfc88ec6a") (current origin/main
SHA; no release tags exist yet) instead of branch: "main".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Crash0v3rrid3
left a comment
There was a problem hiding this comment.
Claude Code Review (automated) — 1 inline finding(s). Full report in the PR comment below. Verdict: Failed - see PR comment.
| # Pinned, immutable git revision the self-update is allowed to fetch from. | ||
| # DEVA11Y-475: never fetch executable code from a mutable branch HEAD. | ||
| # Bump this (and the published .sha256 sidecars) on every release. | ||
| SELF_UPDATE_REF="db817c37cf74cba47e2fef535f53a35bfc88ec6a" |
There was a problem hiding this comment.
[High] Self-update is non-functional as committed
Two problems converge here:
script_self_updatefetches${base_url}.sha256, but no.sha256sidecar files exist anywhere in the repo — so--self-updatealways aborts at the checksum-download step.- This pinned
SELF_UPDATE_REF(db817c37…) is the parent commit of this PR's head, which does not contain the hardened script — so the pin references the old, pre-hardening code rather than the commit that introduces these changes.
It fails safe (aborts rather than rolling back), but the feature can't actually run.
Suggestion: Before merge, (1) generate and commit the six *.sha256 sidecars next to each script, and (2) bump SELF_UPDATE_REF in all 6 scripts to the commit that actually contains the hardened scripts + sidecars (e.g. the merge commit), not the parent.
Reviewer: stack:code-reviewer
Claude Code PR ReviewPR: #30 • Head: fc2a1ef • Reviewers: stack:code-reviewer (+ orchestrator verification) SummaryHardens Review Table
Findings
Verdict: FAIL — strong security direction, but the self-update is non-functional as committed (missing |
…EVA11Y-475,477,478) Addresses PR #30 review. Per maintainer intent, auto-update should always take the latest from main rather than pin to a commit, so: - Revert self-update fetch to main HEAD and restore auto-update on every run (best-effort: script_self_update || true, so offline/integrity failures never block the tool). - Keep SHA-256 verification and commit the 6 .sha256 sidecars so the integrity check actually works against main (regenerate on every script change to main). - Fetch the sidecar first and skip when the on-disk copy already matches (avoids a redundant download/rewrite each run). - Portable hashing: prefer sha256sum, fall back to shasum -a 256; guard empty actual_sum. - Resolve SCRIPT_PATH absolutely so the replace never depends on CWD; stage within the target dir then mv so the replace is atomic on the same filesystem. - Add curl --connect-timeout/--max-time; run the shebang sanity-check after checksum verification; mark the branch/relpath constants readonly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review — browserstack/AccessibilityDevTools#30 (12fc1288)
Summary: PR adds SHA-256 sidecar verification and atomic temp-dir staging to the self-update mechanism across all six shell installers, and pins SPM dependency from branch:"main" to a specific .revision() SHA. However, the implementation diverges fundamentally from the stated DEVA11Y-475/478 fixes: auto-update still fires unconditionally on every invocation, still fetches from mutable refs/heads/main, and the co-located sidecar checksum provides only download-integrity — not supply-chain protection.
Findings (7)
🔴 Critical
-
scripts/bash/cli.sh:178— PR description states "self-update only runs via an explicit--self-updatesubcommand; it no longer runs automatically" as the fix for DEVA11Y-475/F-003 (Critical). Actual code:script_self_update || truestill fires unconditionally on every invocation — no--self-updatesubcommand exists anywhere in the diff. The original automatic execution vector is not eliminated. The|| trueonly ensures failures are non-fatal; it does not make the update opt-in. Either implement the opt-in subcommand gate as described, or update the PR/tickets to accurately reflect this is hardening-in-place rather than remediation. -
scripts/bash/cli.sh:91— PR description claims "fetches from an immutable revision (SELF_UPDATE_REF)" as the DEVA11Y-478/F-006 fix. Actual code:SELF_UPDATE_BRANCH="main"constructs the fetch URL asrefs/heads/main— still a mutable branch HEAD. NoSELF_UPDATE_REFvariable exists anywhere in the diff. An attacker with write access tomaincan push a malicious script and simultaneously push a matching.sha256sidecar; the integrity check passes and the payload executes on every user's next run. The stated immutable-pin fix was not implemented.
🟠 High
-
scripts/bash/cli.sh— SHA-256 sidecar co-located with the script on the same mutable origin provides no supply-chain guarantee. An attacker with push access tomainsatisfies the integrity check trivially by updating both files atomically. True supply-chain protection requires a detached signing key (e.g., cosign, GPG) not stored in the same repo. -
scripts/bash/cli.sh— No CI enforcement keeps.sha256sidecars in sync with scripts. The committed sidecars will become stale on the first subsequent commit that modifies any script without regenerating its sidecar, silently breaking self-update for all users (checksum mismatch → update refused). A CI step runningshasum -a 256 -cagainst each script must accompany this PR.
🟡 Medium
-
scripts/bash/cli.sh:91—SELF_UPDATE_BRANCHandSELF_UPDATE_RELPATHare declaredreadonlyat global script scope. If these scripts are ever sourced (. scripts/bash/cli.sh) rather than executed, thereadonlydeclarations persist in the parent shell; re-sourcing fails with "readonly variable". Move declarations insidescript_self_update()as locals, or add a guard. -
scripts/bash/cli.sh— The fullscript_self_update()body and_self_update_sha256()helper are copied verbatim across all six scripts. OnlySELF_UPDATE_RELPATHand filenames differ. Any future security fix must be applied six times without a mechanism to detect divergence. A sourced lib (scripts/lib/self_update.sh) or CI diff-check between the six copies would prevent silent drift.
🔵 Low
scripts/bash/spm.sh:63— SPM revisiondb817c37is hardcoded in three separatespm.shfiles with no date stamp, no CI pin-staleness check, and no automated bump path. A comment capturing the pin date and a Jira reference to the tag-automation follow-up would reduce future confusion.
Jira: DEVA11Y-475, DEVA11Y-477, DEVA11Y-478, APPSEC-415
Summary
Supply-chain / integrity hardening for the six shell installers under
scripts/{bash,zsh,fish}/{cli,spm}.sh. Umbrella: APPSEC-415.Findings fixed
DEVA11Y-475 / F-003 (Critical) + DEVA11Y-478 / F-006 (High) — self-update from mutable branch HEAD
Previously every invocation auto-ran
script_self_update, whichcurled the script fromrefs/heads/main(mutable branch HEAD), accepted it if it merely started with#!, and overwrote the currently-running script in place. This is remote-code-execution-on-every-run with no integrity check.Fix (applied consistently to all six variants):
--self-updatesubcommand; it no longer runs automatically.SELF_UPDATE_REF) instead of a mutable branch..sha256sidecar and verifies the SHA-256 before applying; refuses on mismatch or missing checksum.mvs into place. Temp dir is cleaned via a function-scopedtrap ... RETURN.DEVA11Y-477 / F-005 (High) — SPM dependency pinned to
branch: "main"The generated
Package.swiftheredoc in the threespm.shfiles pinned the dependency tobranch: "main"(mutable). Now pinned to.revision("db817c37cf74cba47e2fef535f53a35bfc88ec6a")— the currentorigin/mainSHA.Caveats / follow-ups
Package.swifthas no version, so per the task rules I pinned to.revision()with the currentorigin/mainSHA rather than inventing a version. BothSELF_UPDATE_REFand the SPMrevisionshould be bumped to a release tag (.exact("x.y.z")) once tagging is introduced.<script>.sha256sidecar alongside each script at the pinned revision in the repo. Those sidecars are not yet published — they must be generated and committed (e.g.shasum -a 256 scripts/bash/cli.sh > scripts/bash/cli.sh.sha256) as part of the release process before--self-updatewill succeed. Until then self-update safely refuses (fails closed) rather than running unverified code.Overlap note
A sibling change (DEVA11Y-483) is editing the cleanup-trap / temp-dir behavior in the same
spm.shfiles. This PR is scoped strictly to self-update + dependency pinning and does not touch thecleanup/trap cleanup EXIT/setuplogic, so the two PRs are independently reviewable. Expect a trivial merge near thescript_self_updateregion only.Validation
bash -npasses on all six scripts.shellcheck --shell=bashproduces only pre-existing findings (shebangSC2096,EXTRA_ARGS=$@, env-exportSC2155, etc.); the newscript_self_updateblocks are clean (one intentional# shellcheck disable=SC2064on the temp-dir trap).🤖 Generated with Claude Code