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 5 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
sunny-se
left a comment
There was a problem hiding this comment.
LGTM — reviewed via harness stack:pr-review. Self-update hardening is a net security improvement.
Conditions (please address before merge):
- Stale PR body: Description still says "Pinned source: fetches from an immutable revision (
SELF_UPDATE_REF)" but commit 2 reverted toSELF_UPDATE_BRANCH="main"(mutable). Please update the PR body to reflect final state — hardening is in the download/verify/replace mechanism, not source immutability. - Strongly recommend: Add a CI check or pre-commit hook that validates
sha256sum -c scripts/**/*.sha256on every PR touchingscripts/. Without this, the first post-merge script edit will silently break self-update.
Non-blocking notes:
- Same-origin checksum acknowledged as not MITM-proof — acceptable for now.
- SPM
.revision()pin will go stale — tracked for future tagging.
…rs (DEVA11Y-475) Addresses PR #30 review (Sunny, condition 2): adds a GitHub Actions workflow that runs on any PR/main push touching scripts/, failing when a launcher script is missing its .sha256 sidecar or when a sidecar is out of date. Self-update verifies each fetched script against its committed sidecar, so a script edit without a sidecar regen would otherwise silently break self-update (checksum mismatch -> update refused) for all users. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the review @sunny-se — both pre-merge conditions addressed in
Detached-signature supply-chain protection remains out of scope and tracked separately. |
…ntain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Crash0v3rrid3
left a comment
There was a problem hiding this comment.
Claude Code Review (automated) — 3 inline finding(s). Full report in the PR comment below. Verdict: Passed.
| verify-sidecars: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
[Low] Pin actions/checkout to a full SHA (repo convention)
The existing Semgrep.yml SHA-pins all actions (actions/checkout@c85c95e3… # v3.5.3). This @v4 floating tag breaks that convention and GitHub's supply-chain hardening guidance.
Suggestion:
| - uses: actions/checkout@v4 | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 |
Reviewer: stack:code-reviewer
| status=0 | ||
|
|
||
| # 1. Every self-updating script must have a sidecar. | ||
| for script in scripts/**/*.sh; do |
There was a problem hiding this comment.
[Low] Add a zero-scripts guard (defensive)
With nullglob, this loop is a no-op if no *.sh are found and status stays 0. Today a true "no scripts" state is still caught by check-2's empty-sidecar guard (exit 1), but a symmetric guard here makes check-1 self-sufficient.
Suggestion: Before the loop, collect into an array and exit 1 if empty, mirroring the existing ${#sidecars[@]} -eq 0 guard.
Reviewer: stack:code-reviewer
| continue | ||
| fi | ||
| # Sidecars store "<sha256> <basename>", so verify from the script's dir. | ||
| if ( cd "$dir" && sha256sum -c "$(basename "$sidecar")" ); then |
There was a problem hiding this comment.
[Low] Silence sha256sum -c stdout for cleaner logs
sha256sum -c prints <file>: OK/FAILED to stdout, doubling the success line and emitting an un-annotated FAILED. Failures are still surfaced via the else-branch ::error::, so this is log clarity only.
Suggestion: Redirect the check's stdout to /dev/null (sha256sum -c "$(basename "$sidecar")" > /dev/null) and rely on the annotation.
Reviewer: stack:code-reviewer
Claude Code PR ReviewPR: #30 • Head: 4293136 • Reviewers: stack:code-reviewer (+ orchestrator verification) SummaryRe-review after the self-update hardening landed: the 6 launcher-script changes + committed Review Table
Findings
Verification performed
Verdict: PASS — the hardening PR plus its CI guard are correct and the workflow does catch sidecar drift. Only Low-severity polish remains: SHA-pin |
…A11Y-477) The generated Dummy Package.swift was pinned to revision db817c3 (DEVA11Y-477/F-005). Per maintainer intent, the SPM scan should always resolve the latest plugin from main — consistent with self-update now tracking main — so revert the dependency to branch: "main". Regenerated the three spm.sh.sha256 sidecars to match (cli.sh unchanged). Reopens the F-005 mutable-source concern by design; revisit with a release tag once tagging exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude Code PR ReviewPR: #30 • Head: 796017a • Reviewers: stack:code-reviewer (+ orchestrator verification) SummaryIncremental re-review of one commit ( Review Table
FindingsNo new findings in this increment. The revert is valid SwiftPM, all three regenerated Intentional trade-off (not a finding): dropping the Carry-over Low items (pre-existing, already commented on this PR at head
Verification performed
Verdict: PASS — clean, intentional revert; mechanically correct and sidecar-consistent. The three carry-over workflow Lows remain open (non-blocking) and the F-005 mutable-source trade-off is accepted by design. |
sunny-se
left a comment
There was a problem hiding this comment.
Review — DEVA11Y-475/477/478 self-update hardening
Overall: Big upgrade over old self-update (raw curl | echo > file). SHA-256 sidecar verification, atomic rename, shebang sanity check, CI guardrail for checksum drift. Good work.
Main concerns: realpath portability bug on macOS, heavy code duplication (6 identical copies), and || true swallowing integrity violations.
See inline comments for specifics.
M1 (must-fix): realpath --relative-to does not exist on stock macOS
All 6 scripts, line 4: SCRIPT_PATH=$(realpath --relative-to="$GIT_ROOT" "$0" 2>/dev/null || realpath "$0")
Pre-existing bug BUT new code depends on SCRIPT_PATH being correct for self-update target resolution. Stock macOS has no realpath (only via coreutils or newer macOS). Both fallbacks fail silently → SCRIPT_PATH empty → target_path becomes $GIT_ROOT/ or empty → mktemp fails → non-fatal skip. Self-update silently never works on stock macOS.
Fix: Add $0-based fallback:
SCRIPT_PATH=$(realpath --relative-to="$GIT_ROOT" "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")S1 (should-fix): 6 identical copies of ~80-line self-update function
~480 lines pure duplication across all 6 scripts. Bug fix needs 6 edits. CI catches checksum drift but not logic drift between copies.
Suggestion: Extract to scripts/common/self-update.sh and source it. Each script sets SELF_UPDATE_RELPATH before sourcing. Chicken-and-egg for old scripts acknowledged, but right long-term direction.
S4 (should-fix): PR title mentions SPM pinning but it was reverted
Commit 796017a reverts to branch: "main". Title/description misleading. Update to reflect final state.
Nits
N2. EXTRA_ARGS=$@ without quotes (pre-existing, all 6 scripts). Loses quoting on args with spaces. Should be EXTRA_ARGS=("$@").
N4. CI error message shows shasum -a 256 but CI runs Ubuntu where sha256sum is native. Could confuse Linux devs.
Questions
Q1. "Fish" scripts are actually bash scripts invoking fish -lic. Why live under scripts/fish/ rather than scripts/bash-for-fish/?
Q2. Self-update always follows main HEAD. Bad script with correct checksum auto-deploys to all users. Rollback strategy? Consider SELF_UPDATE_ENABLED=true env var override.
Q3. tests/ directory deleted in this PR. Intentional as part of this change, or bundled cleanup?
|
|
||
| # Stage inside the target's directory so the rename is atomic (mv across | ||
| # filesystems would degrade to a non-atomic copy). | ||
| stage_file=$(mktemp "$(dirname "$target_path")/.bs-a11y-update.XXXXXX") || { |
There was a problem hiding this comment.
should-fix: Stale temp files on SIGINT/SIGTERM.
stage_file created here in the target directory. trap ... RETURN cleans tmp_dir but stage_file only cleaned in the else branch of the cp/chmod/mv chain (line 166). If SIGINT between this mktemp and the mv, dotfile (.bs-a11y-update.XXXXXX) leaks in project directory.
Fix: Add stage_file to trap cleanup or set broader trap covering both temp artifacts.
| # Best-effort auto-update: always fetch the latest launcher from main before | ||
| # running. Failures (offline, integrity) are non-fatal -- the current script | ||
| # keeps working and any update applies on the next invocation. | ||
| script_self_update || true |
There was a problem hiding this comment.
should-fix: || true swallows checksum mismatch (potential MITM/corruption) same as network timeout.
User sees stderr message but script continues normally. Consider differentiating return codes:
- Network unreachable → return 0 (skip silently)
- Integrity violation → return 2 (warn harder or abort)
Caller decides based on code. Currently a corrupted download and a flaky network look identical to the caller.
| echo "Self-update: failed to stage update next to ${target_path}." >&2 | ||
| return 1 | ||
| } | ||
| if cp "$tmp_script" "$stage_file" && chmod 0755 "$stage_file" && mv -f "$stage_file" "$target_path"; then |
There was a problem hiding this comment.
nit: chmod 0755 here but download_binary (line 174) uses chmod 0775. Group-write on binary but not on script — intentional? Minor inconsistency.
Summary
Integrity / robustness hardening for the six shell installers under
scripts/{bash,zsh,fish}/{cli,spm}.sh, plus an SPM dependency pin. Umbrella: APPSEC-415 (DEVA11Y-475 / 477 / 478).What changed
Self-update mechanism (
script_self_update, all six scripts) — DEVA11Y-475 / 478Previously every invocation
curled the script fromrefs/heads/main, accepted it if it merely started with#!, and overwrote the currently-running file in place — no integrity check, non-atomic.Now (still auto-running, still from
main, best-effort viascript_self_update || trueso failures never block the tool):<script>.sha256sidecar and verifies the digest before applying; refuses on mismatch/missing checksum (fails closed).mvs into place (same-filesystem atomic), so the running script is never half-written.sha256sum, falls back toshasum -a 256; guards empty output.curl --connect-timeout 10 --max-time 30on both fetches.The six
<script>.sha256sidecars are committed in this PR.SPM dependency pin — DEVA11Y-477 / F-005
The generated
Package.swiftheredoc in the threespm.shfiles pinned the dependency tobranch: "main"(mutable). Now pinned to.revision("db817c37cf74cba47e2fef535f53a35bfc88ec6a")— the currentorigin/mainSHA.CI — keep sidecars in sync (review follow-up)
.github/workflows/verify-selfupdate-checksums.ymlruns on every PR/main push touchingscripts/and fails when a script is missing its.sha256sidecar or when a sidecar is out of date — so a future script edit can't silently break self-update.Known limitations (accepted)
.sha256are served from the sameraw.githubusercontent.com/.../mainorigin, so the checksum guards against truncated/corrupted downloads, not amaincompromise (an attacker with push access updates both together). True supply-chain protection (detached signature / cosign / GPG) is out of scope here and tracked separately..revision()pin will go stale. BumpSELF_UPDATEsource and the SPMrevisionto a release tag (.exact("x.y.z")) once tagging is introduced.Validation
bash -npasses on all six scripts; the self-update logic was traced for the apply / skip-when-current / checksum-mismatch / offline paths..sha256sidecars verified against their scripts (shasum -a 256 -c); the new workflow enforces this going forward.--self-updateagainstmaincan only be exercised once this merges (inherent to self-update-from-main).🤖 Generated with Claude Code