Skip to content

Commit c0b5ede

Browse files
sunny-seclaude
andcommitted
fix(security): harden self-update with tagged refs + SHA-256 verification (DEVA11Y-478)
Self-update previously fetched executable code from refs/heads/main with only a shebang check (CWE-494, CVSS 7.8). Now: 1. Version pointer (latest-version.txt) fetched from main — metadata only 2. Actual script fetched from immutable tagged ref (refs/tags/vX.Y.Z) 3. SHA-256 checksum verified against SHA256SUMS before overwriting Migration: old scripts on dev machines will self-update from main one last time, pulling the new mechanism. All subsequent updates go through the verified tagged path. Post-merge: tag the merge commit as v1.0.0 and push tag immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent db817c3 commit c0b5ede

8 files changed

Lines changed: 217 additions & 24 deletions

File tree

scripts/SHA256SUMS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
6d1569318fb07fe4319b88dd129c78953e6a66ebc39f98bd24ec73a121d4fc1e bash/cli.sh
2+
5754120729dc9e503b7454419fd243297e3392cd57e459f3efe29f06f3c78c2d bash/spm.sh
3+
559b757e67f1057f329911a7ce21cadb9637858bfab4192c673ecd6898f7ab47 fish/cli.sh
4+
929e1418b3e09fc72577dc8e8d62f1fc585e7538ec4f720c0b596e2ab7df2e34 fish/spm.sh
5+
16f67393c7f036a88f25fccd647cfbf286718f0792b7c4accb49adc620168976 zsh/cli.sh
6+
f9c450e90c1f7b1917ef2106d04df7d02775465af1052ba14b211550125000aa zsh/spm.sh

scripts/bash/cli.sh

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash -il
2+
SCRIPT_VERSION="v1.0.0"
23

34
GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
45
SCRIPT_PATH=$(realpath --relative-to="$GIT_ROOT" "$0" 2>/dev/null || realpath "$0")
@@ -79,12 +80,42 @@ a11y_scan() {
7980
}
8081

8182
script_self_update() {
82-
local remote_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/main/scripts/bash/cli.sh"
83+
local repo_base="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools"
84+
local version_url="${repo_base}/refs/heads/main/scripts/latest-version.txt"
85+
local script_rel_path="bash/cli.sh"
8386

84-
updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url")
85-
if [[ $updated_script =~ ^#! ]]; then
86-
echo "$updated_script" > "$SCRIPT_PATH"
87+
# Fetch remote version (lightweight metadata from main, not executable code)
88+
local remote_version
89+
remote_version=$(curl -fsSL --max-time 10 "$version_url" 2>/dev/null | tr -d '[:space:]')
90+
if [[ -z "$remote_version" || "$remote_version" == "$SCRIPT_VERSION" ]]; then
91+
return 0
8792
fi
93+
94+
# Fetch script and checksums from immutable tagged ref
95+
local tag_base="${repo_base}/refs/tags/${remote_version}/scripts"
96+
local tmp_script tmp_sums
97+
tmp_script=$(mktemp)
98+
tmp_sums=$(mktemp)
99+
trap 'rm -f "$tmp_script" "$tmp_sums"' RETURN
100+
101+
if ! curl -fsSL --max-time 30 "${tag_base}/${script_rel_path}" -o "$tmp_script" 2>/dev/null; then
102+
return 0
103+
fi
104+
if ! curl -fsSL --max-time 10 "${tag_base}/SHA256SUMS" -o "$tmp_sums" 2>/dev/null; then
105+
return 0
106+
fi
107+
108+
# Verify SHA-256 checksum
109+
local expected actual
110+
expected=$(grep " ${script_rel_path}$" "$tmp_sums" | cut -d' ' -f1)
111+
actual=$(shasum -a 256 "$tmp_script" | cut -d' ' -f1)
112+
if [[ -z "$expected" || "$actual" != "$expected" ]]; then
113+
echo "[self-update] WARNING: Checksum verification failed for ${script_rel_path}. Update aborted." >&2
114+
return 1
115+
fi
116+
117+
cp "$tmp_script" "$0"
118+
echo "[self-update] Updated to ${remote_version}." >&2
88119
}
89120

90121
download_binary() {

scripts/bash/spm.sh

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash -il
2+
SCRIPT_VERSION="v1.0.0"
23

34
[ -f "${PWD}/Package.swift" ]
45
PACKAGE_EXISTS="$?"
@@ -84,12 +85,42 @@ EOF
8485
}
8586

8687
script_self_update() {
87-
local remote_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/main/scripts/bash/spm.sh"
88+
local repo_base="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools"
89+
local version_url="${repo_base}/refs/heads/main/scripts/latest-version.txt"
90+
local script_rel_path="bash/spm.sh"
8891

89-
updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url")
90-
if [[ $updated_script =~ ^#! ]]; then
91-
echo "$updated_script" > "$SCRIPT_PATH"
92+
# Fetch remote version (lightweight metadata from main, not executable code)
93+
local remote_version
94+
remote_version=$(curl -fsSL --max-time 10 "$version_url" 2>/dev/null | tr -d '[:space:]')
95+
if [[ -z "$remote_version" || "$remote_version" == "$SCRIPT_VERSION" ]]; then
96+
return 0
9297
fi
98+
99+
# Fetch script and checksums from immutable tagged ref
100+
local tag_base="${repo_base}/refs/tags/${remote_version}/scripts"
101+
local tmp_script tmp_sums
102+
tmp_script=$(mktemp)
103+
tmp_sums=$(mktemp)
104+
trap 'rm -f "$tmp_script" "$tmp_sums"' RETURN
105+
106+
if ! curl -fsSL --max-time 30 "${tag_base}/${script_rel_path}" -o "$tmp_script" 2>/dev/null; then
107+
return 0
108+
fi
109+
if ! curl -fsSL --max-time 10 "${tag_base}/SHA256SUMS" -o "$tmp_sums" 2>/dev/null; then
110+
return 0
111+
fi
112+
113+
# Verify SHA-256 checksum
114+
local expected actual
115+
expected=$(grep " ${script_rel_path}$" "$tmp_sums" | cut -d' ' -f1)
116+
actual=$(shasum -a 256 "$tmp_script" | cut -d' ' -f1)
117+
if [[ -z "$expected" || "$actual" != "$expected" ]]; then
118+
echo "[self-update] WARNING: Checksum verification failed for ${script_rel_path}. Update aborted." >&2
119+
return 1
120+
fi
121+
122+
cp "$tmp_script" "$0"
123+
echo "[self-update] Updated to ${remote_version}." >&2
93124
}
94125

95126
script_self_update

scripts/fish/cli.sh

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash -il
2+
SCRIPT_VERSION="v1.0.0"
23

34
export PATH="$PATH:/opt/homebrew/bin"
45
# Shell specific
@@ -91,12 +92,42 @@ a11y_scan() {
9192
}
9293

9394
script_self_update() {
94-
local remote_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/main/scripts/fish/cli.sh"
95+
local repo_base="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools"
96+
local version_url="${repo_base}/refs/heads/main/scripts/latest-version.txt"
97+
local script_rel_path="fish/cli.sh"
98+
99+
# Fetch remote version (lightweight metadata from main, not executable code)
100+
local remote_version
101+
remote_version=$(curl -fsSL --max-time 10 "$version_url" 2>/dev/null | tr -d '[:space:]')
102+
if [[ -z "$remote_version" || "$remote_version" == "$SCRIPT_VERSION" ]]; then
103+
return 0
104+
fi
105+
106+
# Fetch script and checksums from immutable tagged ref
107+
local tag_base="${repo_base}/refs/tags/${remote_version}/scripts"
108+
local tmp_script tmp_sums
109+
tmp_script=$(mktemp)
110+
tmp_sums=$(mktemp)
111+
trap 'rm -f "$tmp_script" "$tmp_sums"' RETURN
112+
113+
if ! curl -fsSL --max-time 30 "${tag_base}/${script_rel_path}" -o "$tmp_script" 2>/dev/null; then
114+
return 0
115+
fi
116+
if ! curl -fsSL --max-time 10 "${tag_base}/SHA256SUMS" -o "$tmp_sums" 2>/dev/null; then
117+
return 0
118+
fi
95119

96-
updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url")
97-
if [[ $updated_script =~ ^#! ]]; then
98-
echo "$updated_script" > "$SCRIPT_PATH"
120+
# Verify SHA-256 checksum
121+
local expected actual
122+
expected=$(grep " ${script_rel_path}$" "$tmp_sums" | cut -d' ' -f1)
123+
actual=$(shasum -a 256 "$tmp_script" | cut -d' ' -f1)
124+
if [[ -z "$expected" || "$actual" != "$expected" ]]; then
125+
echo "[self-update] WARNING: Checksum verification failed for ${script_rel_path}. Update aborted." >&2
126+
return 1
99127
fi
128+
129+
cp "$tmp_script" "$0"
130+
echo "[self-update] Updated to ${remote_version}." >&2
100131
}
101132

102133
download_binary() {

scripts/fish/spm.sh

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash -il
2+
SCRIPT_VERSION="v1.0.0"
23

34
export PATH="$PATH:/opt/homebrew/bin"
45
# Shell specific
@@ -97,12 +98,42 @@ EOF
9798
}
9899

99100
script_self_update() {
100-
local remote_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/main/scripts/fish/spm.sh"
101+
local repo_base="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools"
102+
local version_url="${repo_base}/refs/heads/main/scripts/latest-version.txt"
103+
local script_rel_path="fish/spm.sh"
101104

102-
updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url")
103-
if [[ $updated_script =~ ^#! ]]; then
104-
echo "$updated_script" > "$SCRIPT_PATH"
105+
# Fetch remote version (lightweight metadata from main, not executable code)
106+
local remote_version
107+
remote_version=$(curl -fsSL --max-time 10 "$version_url" 2>/dev/null | tr -d '[:space:]')
108+
if [[ -z "$remote_version" || "$remote_version" == "$SCRIPT_VERSION" ]]; then
109+
return 0
105110
fi
111+
112+
# Fetch script and checksums from immutable tagged ref
113+
local tag_base="${repo_base}/refs/tags/${remote_version}/scripts"
114+
local tmp_script tmp_sums
115+
tmp_script=$(mktemp)
116+
tmp_sums=$(mktemp)
117+
trap 'rm -f "$tmp_script" "$tmp_sums"' RETURN
118+
119+
if ! curl -fsSL --max-time 30 "${tag_base}/${script_rel_path}" -o "$tmp_script" 2>/dev/null; then
120+
return 0
121+
fi
122+
if ! curl -fsSL --max-time 10 "${tag_base}/SHA256SUMS" -o "$tmp_sums" 2>/dev/null; then
123+
return 0
124+
fi
125+
126+
# Verify SHA-256 checksum
127+
local expected actual
128+
expected=$(grep " ${script_rel_path}$" "$tmp_sums" | cut -d' ' -f1)
129+
actual=$(shasum -a 256 "$tmp_script" | cut -d' ' -f1)
130+
if [[ -z "$expected" || "$actual" != "$expected" ]]; then
131+
echo "[self-update] WARNING: Checksum verification failed for ${script_rel_path}. Update aborted." >&2
132+
return 1
133+
fi
134+
135+
cp "$tmp_script" "$0"
136+
echo "[self-update] Updated to ${remote_version}." >&2
106137
}
107138

108139
script_self_update

scripts/latest-version.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v1.0.0

scripts/zsh/cli.sh

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash -il
2+
SCRIPT_VERSION="v1.0.0"
23

34
# Shell specific
45
zsh_bin=$(command -v zsh)
@@ -90,12 +91,42 @@ a11y_scan() {
9091
}
9192

9293
script_self_update() {
93-
local remote_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/main/scripts/zsh/cli.sh"
94+
local repo_base="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools"
95+
local version_url="${repo_base}/refs/heads/main/scripts/latest-version.txt"
96+
local script_rel_path="zsh/cli.sh"
97+
98+
# Fetch remote version (lightweight metadata from main, not executable code)
99+
local remote_version
100+
remote_version=$(curl -fsSL --max-time 10 "$version_url" 2>/dev/null | tr -d '[:space:]')
101+
if [[ -z "$remote_version" || "$remote_version" == "$SCRIPT_VERSION" ]]; then
102+
return 0
103+
fi
104+
105+
# Fetch script and checksums from immutable tagged ref
106+
local tag_base="${repo_base}/refs/tags/${remote_version}/scripts"
107+
local tmp_script tmp_sums
108+
tmp_script=$(mktemp)
109+
tmp_sums=$(mktemp)
110+
trap 'rm -f "$tmp_script" "$tmp_sums"' RETURN
111+
112+
if ! curl -fsSL --max-time 30 "${tag_base}/${script_rel_path}" -o "$tmp_script" 2>/dev/null; then
113+
return 0
114+
fi
115+
if ! curl -fsSL --max-time 10 "${tag_base}/SHA256SUMS" -o "$tmp_sums" 2>/dev/null; then
116+
return 0
117+
fi
94118

95-
updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url")
96-
if [[ $updated_script =~ ^#! ]]; then
97-
echo "$updated_script" > "$SCRIPT_PATH"
119+
# Verify SHA-256 checksum
120+
local expected actual
121+
expected=$(grep " ${script_rel_path}$" "$tmp_sums" | cut -d' ' -f1)
122+
actual=$(shasum -a 256 "$tmp_script" | cut -d' ' -f1)
123+
if [[ -z "$expected" || "$actual" != "$expected" ]]; then
124+
echo "[self-update] WARNING: Checksum verification failed for ${script_rel_path}. Update aborted." >&2
125+
return 1
98126
fi
127+
128+
cp "$tmp_script" "$0"
129+
echo "[self-update] Updated to ${remote_version}." >&2
99130
}
100131

101132
download_binary() {

scripts/zsh/spm.sh

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash -il
2+
SCRIPT_VERSION="v1.0.0"
23

34
# Shell specific
45
zsh_bin=$(command -v zsh)
@@ -96,12 +97,42 @@ EOF
9697
}
9798

9899
script_self_update() {
99-
local remote_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/main/scripts/zsh/spm.sh"
100+
local repo_base="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools"
101+
local version_url="${repo_base}/refs/heads/main/scripts/latest-version.txt"
102+
local script_rel_path="zsh/spm.sh"
100103

101-
updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url")
102-
if [[ $updated_script =~ ^#! ]]; then
103-
echo "$updated_script" > "$SCRIPT_PATH"
104+
# Fetch remote version (lightweight metadata from main, not executable code)
105+
local remote_version
106+
remote_version=$(curl -fsSL --max-time 10 "$version_url" 2>/dev/null | tr -d '[:space:]')
107+
if [[ -z "$remote_version" || "$remote_version" == "$SCRIPT_VERSION" ]]; then
108+
return 0
104109
fi
110+
111+
# Fetch script and checksums from immutable tagged ref
112+
local tag_base="${repo_base}/refs/tags/${remote_version}/scripts"
113+
local tmp_script tmp_sums
114+
tmp_script=$(mktemp)
115+
tmp_sums=$(mktemp)
116+
trap 'rm -f "$tmp_script" "$tmp_sums"' RETURN
117+
118+
if ! curl -fsSL --max-time 30 "${tag_base}/${script_rel_path}" -o "$tmp_script" 2>/dev/null; then
119+
return 0
120+
fi
121+
if ! curl -fsSL --max-time 10 "${tag_base}/SHA256SUMS" -o "$tmp_sums" 2>/dev/null; then
122+
return 0
123+
fi
124+
125+
# Verify SHA-256 checksum
126+
local expected actual
127+
expected=$(grep " ${script_rel_path}$" "$tmp_sums" | cut -d' ' -f1)
128+
actual=$(shasum -a 256 "$tmp_script" | cut -d' ' -f1)
129+
if [[ -z "$expected" || "$actual" != "$expected" ]]; then
130+
echo "[self-update] WARNING: Checksum verification failed for ${script_rel_path}. Update aborted." >&2
131+
return 1
132+
fi
133+
134+
cp "$tmp_script" "$0"
135+
echo "[self-update] Updated to ${remote_version}." >&2
105136
}
106137

107138
script_self_update

0 commit comments

Comments
 (0)