From c6a7527248c83568fc83d3566a567a2b84a1b297 Mon Sep 17 00:00:00 2001 From: Crash0v3rrid3 Date: Thu, 30 Jul 2026 15:11:20 +0530 Subject: [PATCH 1/2] fix(self-update): make script self-update opt-in, not automatic (DEVA11Y-475) The launcher scripts fetched and applied a self-update on every invocation, before subcommand parsing. Integrity verification (SHA-256 + atomic staging) already landed on main, but the unconditional run meant a single compromise of the fetched source would still silently replace the running script on every developer machine with no way to opt out. Gate self-update behind an explicit `self-update` subcommand across all six launchers (bash/zsh/fish x cli/spm) and document it in the README. Note: the update source is still pinned to the mutable refs/heads/main; pinning to an immutable signed tag + per-tag checksum is follow-up work that depends on a release/tagging pipeline (tracked under APPSEC-415). Co-Authored-By: Claude Opus 4.8 --- README.md | 19 +++++++++++++++++++ scripts/bash/cli.sh | 23 +++++++++++++---------- scripts/bash/spm.sh | 23 +++++++++++++---------- scripts/fish/cli.sh | 23 +++++++++++++---------- scripts/fish/spm.sh | 23 +++++++++++++---------- scripts/zsh/cli.sh | 23 +++++++++++++---------- scripts/zsh/spm.sh | 23 +++++++++++++---------- 7 files changed, 97 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 1d74d62..0fa6d4d 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,25 @@ Fish You can then edit the `.git/hooks/pre-commit` file to customise the registered pre-commit hook. +--- +## Updating the launcher script +The launcher scripts no longer update themselves automatically on every run. To pull the latest verified launcher on demand, run the `self-update` subcommand — the download is checked against a published SHA-256 checksum and is only applied if it matches. + +Zsh +```zsh +./browserstack-a11y-scan-spm-zsh.sh self-update +``` + +Bash +```bash +./browserstack-a11y-scan-spm-bash.sh self-update +``` + +Fish +```bash +./browserstack-a11y-scan-spm-fish.sh self-update +``` + --- ## Support For any issues or feedback, reach out to support@browserstack.com \ No newline at end of file diff --git a/scripts/bash/cli.sh b/scripts/bash/cli.sh index 76f7b0b..f0b339c 100644 --- a/scripts/bash/cli.sh +++ b/scripts/bash/cli.sh @@ -183,16 +183,19 @@ download_binary() { bsdtar -xvf "$BINARY_ZIP_PATH" -O > "$BINARY_PATH" && chmod 0775 "$BINARY_PATH" } -# Best-effort auto-update: always fetch the latest launcher from main before -# running. Network/offline failures are silent (rc 0) and operational errors -# (rc 1) are non-fatal -- the existing script keeps working. An integrity -# failure (rc 2: checksum mismatch or non-script payload) leaves the verified -# on-disk script untouched and is surfaced loudly below, but still does not -# block the tool (per the always-run-latest, never-block design). -_self_update_rc=0 -script_self_update || _self_update_rc=$? -if [[ "$_self_update_rc" -eq 2 ]]; then - echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 +# Self-update is opt-in (DEVA11Y-475): it runs only via the explicit `self-update` +# subcommand, never automatically on every invocation. Running it unconditionally +# before subcommand parsing meant a single compromise of the fetched source silently +# replaced the running script on every developer's machine, with no way to opt out; +# gating it behind an explicit command removes that always-on side-effect. Integrity +# verification (SHA-256 check + atomic staging) still guards the download itself. +if [[ $SUBCOMMAND == "self-update" ]]; then + _self_update_rc=0 + script_self_update || _self_update_rc=$? + if [[ "$_self_update_rc" -eq 2 ]]; then + echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 + fi + exit "$_self_update_rc" fi if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then diff --git a/scripts/bash/spm.sh b/scripts/bash/spm.sh index 0fcb69f..d63965f 100644 --- a/scripts/bash/spm.sh +++ b/scripts/bash/spm.sh @@ -183,16 +183,19 @@ script_self_update() { fi } -# Best-effort auto-update: always fetch the latest launcher from main before -# running. Network/offline failures are silent (rc 0) and operational errors -# (rc 1) are non-fatal -- the existing script keeps working. An integrity -# failure (rc 2: checksum mismatch or non-script payload) leaves the verified -# on-disk script untouched and is surfaced loudly below, but still does not -# block the tool (per the always-run-latest, never-block design). -_self_update_rc=0 -script_self_update || _self_update_rc=$? -if [[ "$_self_update_rc" -eq 2 ]]; then - echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 +# Self-update is opt-in (DEVA11Y-475): it runs only via the explicit `self-update` +# subcommand, never automatically on every invocation. Running it unconditionally +# before subcommand parsing meant a single compromise of the fetched source silently +# replaced the running script on every developer's machine, with no way to opt out; +# gating it behind an explicit command removes that always-on side-effect. Integrity +# verification (SHA-256 check + atomic staging) still guards the download itself. +if [[ $SUBCOMMAND == "self-update" ]]; then + _self_update_rc=0 + script_self_update || _self_update_rc=$? + if [[ "$_self_update_rc" -eq 2 ]]; then + echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 + fi + exit "$_self_update_rc" fi if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then diff --git a/scripts/fish/cli.sh b/scripts/fish/cli.sh index 8848880..5d46cfd 100644 --- a/scripts/fish/cli.sh +++ b/scripts/fish/cli.sh @@ -195,16 +195,19 @@ download_binary() { bsdtar -xvf "$BINARY_ZIP_PATH" -O > "$BINARY_PATH" && chmod 0775 "$BINARY_PATH" } -# Best-effort auto-update: always fetch the latest launcher from main before -# running. Network/offline failures are silent (rc 0) and operational errors -# (rc 1) are non-fatal -- the existing script keeps working. An integrity -# failure (rc 2: checksum mismatch or non-script payload) leaves the verified -# on-disk script untouched and is surfaced loudly below, but still does not -# block the tool (per the always-run-latest, never-block design). -_self_update_rc=0 -script_self_update || _self_update_rc=$? -if [[ "$_self_update_rc" -eq 2 ]]; then - echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 +# Self-update is opt-in (DEVA11Y-475): it runs only via the explicit `self-update` +# subcommand, never automatically on every invocation. Running it unconditionally +# before subcommand parsing meant a single compromise of the fetched source silently +# replaced the running script on every developer's machine, with no way to opt out; +# gating it behind an explicit command removes that always-on side-effect. Integrity +# verification (SHA-256 check + atomic staging) still guards the download itself. +if [[ $SUBCOMMAND == "self-update" ]]; then + _self_update_rc=0 + script_self_update || _self_update_rc=$? + if [[ "$_self_update_rc" -eq 2 ]]; then + echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 + fi + exit "$_self_update_rc" fi if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then diff --git a/scripts/fish/spm.sh b/scripts/fish/spm.sh index 34b1079..8bac0b9 100644 --- a/scripts/fish/spm.sh +++ b/scripts/fish/spm.sh @@ -196,16 +196,19 @@ script_self_update() { fi } -# Best-effort auto-update: always fetch the latest launcher from main before -# running. Network/offline failures are silent (rc 0) and operational errors -# (rc 1) are non-fatal -- the existing script keeps working. An integrity -# failure (rc 2: checksum mismatch or non-script payload) leaves the verified -# on-disk script untouched and is surfaced loudly below, but still does not -# block the tool (per the always-run-latest, never-block design). -_self_update_rc=0 -script_self_update || _self_update_rc=$? -if [[ "$_self_update_rc" -eq 2 ]]; then - echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 +# Self-update is opt-in (DEVA11Y-475): it runs only via the explicit `self-update` +# subcommand, never automatically on every invocation. Running it unconditionally +# before subcommand parsing meant a single compromise of the fetched source silently +# replaced the running script on every developer's machine, with no way to opt out; +# gating it behind an explicit command removes that always-on side-effect. Integrity +# verification (SHA-256 check + atomic staging) still guards the download itself. +if [[ $SUBCOMMAND == "self-update" ]]; then + _self_update_rc=0 + script_self_update || _self_update_rc=$? + if [[ "$_self_update_rc" -eq 2 ]]; then + echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 + fi + exit "$_self_update_rc" fi if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then diff --git a/scripts/zsh/cli.sh b/scripts/zsh/cli.sh index 01892ad..ad20986 100644 --- a/scripts/zsh/cli.sh +++ b/scripts/zsh/cli.sh @@ -194,16 +194,19 @@ download_binary() { bsdtar -xvf "$BINARY_ZIP_PATH" -O > "$BINARY_PATH" && chmod 0775 "$BINARY_PATH" } -# Best-effort auto-update: always fetch the latest launcher from main before -# running. Network/offline failures are silent (rc 0) and operational errors -# (rc 1) are non-fatal -- the existing script keeps working. An integrity -# failure (rc 2: checksum mismatch or non-script payload) leaves the verified -# on-disk script untouched and is surfaced loudly below, but still does not -# block the tool (per the always-run-latest, never-block design). -_self_update_rc=0 -script_self_update || _self_update_rc=$? -if [[ "$_self_update_rc" -eq 2 ]]; then - echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 +# Self-update is opt-in (DEVA11Y-475): it runs only via the explicit `self-update` +# subcommand, never automatically on every invocation. Running it unconditionally +# before subcommand parsing meant a single compromise of the fetched source silently +# replaced the running script on every developer's machine, with no way to opt out; +# gating it behind an explicit command removes that always-on side-effect. Integrity +# verification (SHA-256 check + atomic staging) still guards the download itself. +if [[ $SUBCOMMAND == "self-update" ]]; then + _self_update_rc=0 + script_self_update || _self_update_rc=$? + if [[ "$_self_update_rc" -eq 2 ]]; then + echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 + fi + exit "$_self_update_rc" fi if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then diff --git a/scripts/zsh/spm.sh b/scripts/zsh/spm.sh index ac963bd..66a210e 100644 --- a/scripts/zsh/spm.sh +++ b/scripts/zsh/spm.sh @@ -195,16 +195,19 @@ script_self_update() { fi } -# Best-effort auto-update: always fetch the latest launcher from main before -# running. Network/offline failures are silent (rc 0) and operational errors -# (rc 1) are non-fatal -- the existing script keeps working. An integrity -# failure (rc 2: checksum mismatch or non-script payload) leaves the verified -# on-disk script untouched and is surfaced loudly below, but still does not -# block the tool (per the always-run-latest, never-block design). -_self_update_rc=0 -script_self_update || _self_update_rc=$? -if [[ "$_self_update_rc" -eq 2 ]]; then - echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 +# Self-update is opt-in (DEVA11Y-475): it runs only via the explicit `self-update` +# subcommand, never automatically on every invocation. Running it unconditionally +# before subcommand parsing meant a single compromise of the fetched source silently +# replaced the running script on every developer's machine, with no way to opt out; +# gating it behind an explicit command removes that always-on side-effect. Integrity +# verification (SHA-256 check + atomic staging) still guards the download itself. +if [[ $SUBCOMMAND == "self-update" ]]; then + _self_update_rc=0 + script_self_update || _self_update_rc=$? + if [[ "$_self_update_rc" -eq 2 ]]; then + echo "Self-update: integrity verification FAILED; kept the existing verified script (possible corruption or tampering)." >&2 + fi + exit "$_self_update_rc" fi if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then From 0f881c4c7ca675bdf27be9d9c0ccb2ac3001389c Mon Sep 17 00:00:00 2001 From: Crash0v3rrid3 Date: Thu, 30 Jul 2026 15:48:44 +0530 Subject: [PATCH 2/2] fix(review): regenerate checksum sidecars + correct stale docs (DEVA11Y-475) Code review caught that the previous commit edited all six launchers but left their .sha256 sidecars stale, which would make the new `self-update` command fail the integrity check on every run (dead on arrival). Regenerate all six sidecars to match the edited scripts. Also: update the pre-existing header comment that still claimed self-update runs automatically ('always run the newest version'), and soften the README to describe the checksum as a transit-integrity check rather than an authenticity signature (script and checksum share one origin on refs/heads/main). Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- scripts/bash/cli.sh | 7 ++++--- scripts/bash/cli.sh.sha256 | 2 +- scripts/bash/spm.sh | 7 ++++--- scripts/bash/spm.sh.sha256 | 2 +- scripts/fish/cli.sh | 7 ++++--- scripts/fish/cli.sh.sha256 | 2 +- scripts/fish/spm.sh | 7 ++++--- scripts/fish/spm.sh.sha256 | 2 +- scripts/zsh/cli.sh | 7 ++++--- scripts/zsh/cli.sh.sha256 | 2 +- scripts/zsh/spm.sh | 7 ++++--- scripts/zsh/spm.sh.sha256 | 2 +- 13 files changed, 31 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0fa6d4d..64ef9b5 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ You can then edit the `.git/hooks/pre-commit` file to customise the registered p --- ## Updating the launcher script -The launcher scripts no longer update themselves automatically on every run. To pull the latest verified launcher on demand, run the `self-update` subcommand — the download is checked against a published SHA-256 checksum and is only applied if it matches. +The launcher scripts no longer update themselves automatically on every run. To pull the latest launcher on demand, run the `self-update` subcommand — the download is checked against a published SHA-256 checksum (an integrity check against corruption in transit, not an authenticity signature, since the script and its checksum share one origin) and is only applied if it matches. Zsh ```zsh diff --git a/scripts/bash/cli.sh b/scripts/bash/cli.sh index f0b339c..4472c07 100644 --- a/scripts/bash/cli.sh +++ b/scripts/bash/cli.sh @@ -78,9 +78,10 @@ a11y_scan() { $BINARY_PATH a11y $EXTRA_ARGS } -# Self-update tracks the latest launcher on `main` so users always run the -# newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather -# than a pinned revision (per maintainer intent: always take the latest). +# Self-update pulls the latest launcher from `main` on demand: it runs only via +# the explicit `self-update` subcommand (DEVA11Y-475), not automatically on every +# invocation. DEVA11Y-477/478: when it does run we deliberately follow main HEAD +# rather than a pinned revision (per maintainer intent: take the latest on demand). # Hardening retained from the pinning work: download to a temp dir, verify a # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature -- # script and checksum share one origin), sanity-check the shebang, then diff --git a/scripts/bash/cli.sh.sha256 b/scripts/bash/cli.sh.sha256 index 7e28cdb..f6165bd 100644 --- a/scripts/bash/cli.sh.sha256 +++ b/scripts/bash/cli.sh.sha256 @@ -1 +1 @@ -2340d95e6ce35ea8656acb02e0b524eeccca02c12a940d44e8cd0c0032b0efdd cli.sh +e233e16b32a0ec18d24acf32e8c415d26c4eeecdc287452117fe51de2f4791dc cli.sh diff --git a/scripts/bash/spm.sh b/scripts/bash/spm.sh index d63965f..15ab5cb 100644 --- a/scripts/bash/spm.sh +++ b/scripts/bash/spm.sh @@ -83,9 +83,10 @@ EOF scan $EXTRA_ARGS } -# Self-update tracks the latest launcher on `main` so users always run the -# newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather -# than a pinned revision (per maintainer intent: always take the latest). +# Self-update pulls the latest launcher from `main` on demand: it runs only via +# the explicit `self-update` subcommand (DEVA11Y-475), not automatically on every +# invocation. DEVA11Y-477/478: when it does run we deliberately follow main HEAD +# rather than a pinned revision (per maintainer intent: take the latest on demand). # Hardening retained from the pinning work: download to a temp dir, verify a # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature -- # script and checksum share one origin), sanity-check the shebang, then diff --git a/scripts/bash/spm.sh.sha256 b/scripts/bash/spm.sh.sha256 index c8541fa..07a1839 100644 --- a/scripts/bash/spm.sh.sha256 +++ b/scripts/bash/spm.sh.sha256 @@ -1 +1 @@ -2d627fbe06991da445f7fbcd8bc8c4f5c4dd126c04372f74dd0f4ab39222c57e spm.sh +751b9ac01088fbf8d1a46b179ebea30d4f7691157233652c6b90c078b188f667 spm.sh diff --git a/scripts/fish/cli.sh b/scripts/fish/cli.sh index 5d46cfd..a069b36 100644 --- a/scripts/fish/cli.sh +++ b/scripts/fish/cli.sh @@ -90,9 +90,10 @@ a11y_scan() { $BINARY_PATH a11y $EXTRA_ARGS } -# Self-update tracks the latest launcher on `main` so users always run the -# newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather -# than a pinned revision (per maintainer intent: always take the latest). +# Self-update pulls the latest launcher from `main` on demand: it runs only via +# the explicit `self-update` subcommand (DEVA11Y-475), not automatically on every +# invocation. DEVA11Y-477/478: when it does run we deliberately follow main HEAD +# rather than a pinned revision (per maintainer intent: take the latest on demand). # Hardening retained from the pinning work: download to a temp dir, verify a # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature -- # script and checksum share one origin), sanity-check the shebang, then diff --git a/scripts/fish/cli.sh.sha256 b/scripts/fish/cli.sh.sha256 index 31f4280..1a6478f 100644 --- a/scripts/fish/cli.sh.sha256 +++ b/scripts/fish/cli.sh.sha256 @@ -1 +1 @@ -a2bf0be4a37272548c13a25714e42152de11c31590122880a6ce8c50a55c075b cli.sh +332c8583f95291bf72ac4199702697656d28f5b98eba3a6a724a0272ecf77f83 cli.sh diff --git a/scripts/fish/spm.sh b/scripts/fish/spm.sh index 8bac0b9..6e08190 100644 --- a/scripts/fish/spm.sh +++ b/scripts/fish/spm.sh @@ -96,9 +96,10 @@ EOF scan $EXTRA_ARGS } -# Self-update tracks the latest launcher on `main` so users always run the -# newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather -# than a pinned revision (per maintainer intent: always take the latest). +# Self-update pulls the latest launcher from `main` on demand: it runs only via +# the explicit `self-update` subcommand (DEVA11Y-475), not automatically on every +# invocation. DEVA11Y-477/478: when it does run we deliberately follow main HEAD +# rather than a pinned revision (per maintainer intent: take the latest on demand). # Hardening retained from the pinning work: download to a temp dir, verify a # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature -- # script and checksum share one origin), sanity-check the shebang, then diff --git a/scripts/fish/spm.sh.sha256 b/scripts/fish/spm.sh.sha256 index 6617e4d..c1dfc2c 100644 --- a/scripts/fish/spm.sh.sha256 +++ b/scripts/fish/spm.sh.sha256 @@ -1 +1 @@ -8fe00a7635d3ad66d7197994631c1a58581d4b3a845453f72e5e237338dea24e spm.sh +0b54691444c32682e9a14c301ac332d2c13d0874698d009272d35a123e5075fb spm.sh diff --git a/scripts/zsh/cli.sh b/scripts/zsh/cli.sh index ad20986..8d11b0e 100644 --- a/scripts/zsh/cli.sh +++ b/scripts/zsh/cli.sh @@ -89,9 +89,10 @@ a11y_scan() { $BINARY_PATH a11y $EXTRA_ARGS } -# Self-update tracks the latest launcher on `main` so users always run the -# newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather -# than a pinned revision (per maintainer intent: always take the latest). +# Self-update pulls the latest launcher from `main` on demand: it runs only via +# the explicit `self-update` subcommand (DEVA11Y-475), not automatically on every +# invocation. DEVA11Y-477/478: when it does run we deliberately follow main HEAD +# rather than a pinned revision (per maintainer intent: take the latest on demand). # Hardening retained from the pinning work: download to a temp dir, verify a # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature -- # script and checksum share one origin), sanity-check the shebang, then diff --git a/scripts/zsh/cli.sh.sha256 b/scripts/zsh/cli.sh.sha256 index 7853e23..445f73c 100644 --- a/scripts/zsh/cli.sh.sha256 +++ b/scripts/zsh/cli.sh.sha256 @@ -1 +1 @@ -0a6efbbe2e0578b6e8037bb149145148d2767208636c3899d0b275a319073263 cli.sh +67edf6abb80741b1f723f07533448288de7652aba859499d287672e8c6c299b4 cli.sh diff --git a/scripts/zsh/spm.sh b/scripts/zsh/spm.sh index 66a210e..0f07ffa 100644 --- a/scripts/zsh/spm.sh +++ b/scripts/zsh/spm.sh @@ -95,9 +95,10 @@ EOF scan $EXTRA_ARGS } -# Self-update tracks the latest launcher on `main` so users always run the -# newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather -# than a pinned revision (per maintainer intent: always take the latest). +# Self-update pulls the latest launcher from `main` on demand: it runs only via +# the explicit `self-update` subcommand (DEVA11Y-475), not automatically on every +# invocation. DEVA11Y-477/478: when it does run we deliberately follow main HEAD +# rather than a pinned revision (per maintainer intent: take the latest on demand). # Hardening retained from the pinning work: download to a temp dir, verify a # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature -- # script and checksum share one origin), sanity-check the shebang, then diff --git a/scripts/zsh/spm.sh.sha256 b/scripts/zsh/spm.sh.sha256 index 4c067f0..f098757 100644 --- a/scripts/zsh/spm.sh.sha256 +++ b/scripts/zsh/spm.sh.sha256 @@ -1 +1 @@ -507bb44d5a17a4e15f59fcd3cdc04692a39a65abc2e4bf5375340939ce484a98 spm.sh +d305feab7c9d5f023087fbde25a4d8bbbff1376a3f10f1b25f73166f4f0ac038 spm.sh