From 98210a6fc7cd70147f1a5697cc3c7d9dcb2031a7 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 17 Apr 2026 16:40:04 -0400 Subject: [PATCH 1/3] chore(install): verify downloaded tarball integrity + enforce HTTPS Tightens install.sh so the downloaded tarball is checked against the integrity value the npm registry publishes for that specific version. Changes: * Consolidated curl/wget into `fetch_url` and `fetch_url_to_file` helpers that pass `--proto =https --tlsv1.2` (curl) or `--https-only` (wget) so we don't silently follow an http redirect. * After download, compute the SSRI-style hash (e.g. `sha512-`) of the tarball and compare to the value returned by `GET //`. Mismatch aborts before extraction. * Download the tarball into a `mktemp` file outside the install dir so a failed verification can't leave a partial blob where a later run might trust it; an `EXIT` trap cleans up. Behavior for users on a successful install is unchanged. --- install.sh | 117 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 99 insertions(+), 18 deletions(-) diff --git a/install.sh b/install.sh index 9ecb4287b..236398e2c 100755 --- a/install.sh +++ b/install.sh @@ -115,17 +115,14 @@ detect_platform() { echo "${os}-${arch}${libc_suffix}" } -# Get the latest version from npm registry. -get_latest_version() { - local package_name="$1" - local version +# Fetch a URL to stdout, enforcing HTTPS. +fetch_url() { + local url="$1" - # Try using curl with npm registry API. if command -v curl &> /dev/null; then - version=$(curl -fsSL "https://registry.npmjs.org/${package_name}/latest" | grep -o '"version": *"[^"]*"' | head -1 | sed 's/"version": *"\([^"]*\)"/\1/') - # Fallback to wget. + curl --proto '=https' --tlsv1.2 -fsSL "$url" elif command -v wget &> /dev/null; then - version=$(wget -qO- "https://registry.npmjs.org/${package_name}/latest" | grep -o '"version": *"[^"]*"' | head -1 | sed 's/"version": *"\([^"]*\)"/\1/') + wget --https-only -qO- "$url" else error "Neither curl nor wget found on your system" echo "" @@ -135,6 +132,29 @@ get_latest_version() { info " Fedora: sudo dnf install curl" exit 1 fi +} + +# Download a URL to a file, enforcing HTTPS. +fetch_url_to_file() { + local url="$1" + local out="$2" + + if command -v curl &> /dev/null; then + curl --proto '=https' --tlsv1.2 -fsSL -o "$out" "$url" + elif command -v wget &> /dev/null; then + wget --https-only -qO "$out" "$url" + else + error "Neither curl nor wget found on your system" + exit 1 + fi +} + +# Get the latest version from npm registry. +get_latest_version() { + local package_name="$1" + local version + + version=$(fetch_url "https://registry.npmjs.org/${package_name}/latest" | grep -o '"version": *"[^"]*"' | head -1 | sed 's/"version": *"\([^"]*\)"/\1/') if [ -z "$version" ]; then error "Failed to fetch latest version from npm registry" @@ -147,6 +167,39 @@ get_latest_version() { echo "$version" } +# Get the npm-published integrity string (SSRI format, e.g. "sha512-...") for +# a specific version. +get_published_integrity() { + local package_name="$1" + local version="$2" + local integrity + + integrity=$(fetch_url "https://registry.npmjs.org/${package_name}/${version}" | grep -o '"integrity": *"[^"]*"' | head -1 | sed 's/"integrity": *"\([^"]*\)"/\1/') + + echo "$integrity" +} + +# Compute an SSRI-style hash (e.g. "sha512-") of a file. +compute_integrity() { + local file="$1" + local algo="$2" + local digest + + if command -v openssl &> /dev/null; then + digest=$(openssl dgst "-${algo}" -binary "$file" | base64 | tr -d '\n') + elif [ "$algo" = "sha512" ] && command -v shasum &> /dev/null; then + # Fallback: shasum prints hex; convert to base64. + digest=$(shasum -a 512 "$file" | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n') + elif [ "$algo" = "sha256" ] && command -v shasum &> /dev/null; then + digest=$(shasum -a 256 "$file" | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n') + else + error "No tool available to compute ${algo} (need openssl or shasum)" + exit 1 + fi + + echo "${algo}-${digest}" +} + # Calculate SHA256 hash of a string. calculate_hash() { local str="$1" @@ -201,16 +254,43 @@ install_socket_cli() { # Create installation directory. mkdir -p "$install_dir" - # Download tarball to temporary location. - local temp_tarball="${install_dir}/socket.tgz" - - if command -v curl &> /dev/null; then - curl -fsSL -o "$temp_tarball" "$download_url" - elif command -v wget &> /dev/null; then - wget -qO "$temp_tarball" "$download_url" + # Look up the integrity string the registry published for this exact version. + step "Fetching published integrity..." + local expected_integrity + expected_integrity=$(get_published_integrity "$package_name" "$version") + if [ -z "$expected_integrity" ]; then + error "No integrity found in the npm registry metadata for ${package_name}@${version}" + info "Refusing to install without a published checksum to verify against." + exit 1 fi - success "Package downloaded successfully" + # Algorithm prefix from the SSRI string (e.g. "sha512-..." -> "sha512"). + local integrity_algo="${expected_integrity%%-*}" + + # Download tarball to a temporary location outside the install dir so a + # failed verify can't leave a partial blob where future runs might trust it. + local temp_tarball + if command -v mktemp &> /dev/null; then + temp_tarball=$(mktemp -t socket-cli.XXXXXX.tgz 2>/dev/null || mktemp "${TMPDIR:-/tmp}/socket-cli.XXXXXX") + else + temp_tarball="${TMPDIR:-/tmp}/socket-cli.$$.tgz" + fi + trap 'rm -f "$temp_tarball"' EXIT + + fetch_url_to_file "$download_url" "$temp_tarball" + + # Verify integrity against the value npm published for this version. + step "Verifying integrity..." + local actual_integrity + actual_integrity=$(compute_integrity "$temp_tarball" "$integrity_algo") + if [ "$actual_integrity" != "$expected_integrity" ]; then + error "Integrity check failed for ${package_name}@${version}" + info " expected: ${expected_integrity}" + info " got: ${actual_integrity}" + info "Not installing. Please retry; if this persists, open an issue." + exit 1 + fi + success "Integrity verified (${integrity_algo})" # Extract tarball. step "Capturing lightning in a bottle ⚡" @@ -250,8 +330,9 @@ install_socket_cli() { fi fi - # Clean up tarball. - rm "$temp_tarball" + # Clean up tarball (EXIT trap also handles this in error paths). + rm -f "$temp_tarball" + trap - EXIT success "Binary ready at ${BOLD}$binary_path${NC}" From 4e3a9e16756b160590c5c340e786b636e3b35a3a Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 17 Apr 2026 16:57:38 -0400 Subject: [PATCH 2/3] fix(install): harden integrity parsing and drop xxd dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-up fixes flagged by Cursor bugbot on #1221: 1. `get_published_integrity` pipeline tripped `set -e` under `pipefail` when npm metadata was truncated/malformed. Extract the body first, then parse via a `parse_json_string` helper that tolerates a missing field (returns empty). The caller's "refusing to install without a published checksum" message now fires with context instead of the script exiting silently. 2. `compute_integrity` relied on `xxd -r -p` in the `shasum` fallback branch to convert hex to binary. `xxd` isn't POSIX — it ships with vim-common and is often absent on minimal/Alpine containers. With `set -e` the missing binary killed the script before the friendly else-branch error could print. Replaced the fallback stack with a single openssl-only path. openssl is ubiquitous (macOS, mainstream Linux, default Alpine image, WSL, Git Bash); requiring it removes the portability bug and simplifies the function. If openssl is genuinely missing the script now prints a platform-specific install hint. Verified locally: * happy path (real install) still passes * simulated missing-integrity response prints the helpful message * tampered-download still fails with expected vs got mismatch --- install.sh | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/install.sh b/install.sh index 236398e2c..fcfd9bb75 100755 --- a/install.sh +++ b/install.sh @@ -149,12 +149,27 @@ fetch_url_to_file() { fi } +# Parse a JSON string field out of a response body. Tolerates a missing +# field by returning empty, rather than dying under `pipefail`. +parse_json_string() { + local body="$1" + local field="$2" + # Pipe through `cat` so a grep non-match (exit 1) doesn't trip pipefail; + # the final `echo` replaces an empty match with empty string. + printf '%s' "$body" \ + | grep -o "\"${field}\": *\"[^\"]*\"" \ + | head -1 \ + | sed "s/\"${field}\": *\"\\([^\"]*\\)\"/\\1/" \ + || true +} + # Get the latest version from npm registry. get_latest_version() { local package_name="$1" - local version + local body version - version=$(fetch_url "https://registry.npmjs.org/${package_name}/latest" | grep -o '"version": *"[^"]*"' | head -1 | sed 's/"version": *"\([^"]*\)"/\1/') + body=$(fetch_url "https://registry.npmjs.org/${package_name}/latest") + version=$(parse_json_string "$body" "version") if [ -z "$version" ]; then error "Failed to fetch latest version from npm registry" @@ -172,31 +187,33 @@ get_latest_version() { get_published_integrity() { local package_name="$1" local version="$2" - local integrity + local body - integrity=$(fetch_url "https://registry.npmjs.org/${package_name}/${version}" | grep -o '"integrity": *"[^"]*"' | head -1 | sed 's/"integrity": *"\([^"]*\)"/\1/') - - echo "$integrity" + body=$(fetch_url "https://registry.npmjs.org/${package_name}/${version}") + parse_json_string "$body" "integrity" } # Compute an SSRI-style hash (e.g. "sha512-") of a file. +# Requires `openssl` — the tool is ubiquitous (macOS, every mainstream +# Linux distro, Alpine's default image, WSL, Git Bash) and gives us a +# one-step hex-less pipeline so we don't depend on `xxd` (not POSIX). compute_integrity() { local file="$1" local algo="$2" local digest - if command -v openssl &> /dev/null; then - digest=$(openssl dgst "-${algo}" -binary "$file" | base64 | tr -d '\n') - elif [ "$algo" = "sha512" ] && command -v shasum &> /dev/null; then - # Fallback: shasum prints hex; convert to base64. - digest=$(shasum -a 512 "$file" | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n') - elif [ "$algo" = "sha256" ] && command -v shasum &> /dev/null; then - digest=$(shasum -a 256 "$file" | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n') - else - error "No tool available to compute ${algo} (need openssl or shasum)" + if ! command -v openssl &> /dev/null; then + error "openssl not found — required to verify the download integrity" + echo "" + info "Install openssl and re-run:" + info " macOS: already installed (or: brew install openssl)" + info " Alpine: apk add openssl" + info " Debian: sudo apt-get install openssl" + info " Fedora: sudo dnf install openssl" exit 1 fi + digest=$(openssl dgst "-${algo}" -binary "$file" | openssl base64 -A) echo "${algo}-${digest}" } From cec6b1c6b216d47c15acb5e1ac60e544851c3957 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 17 Apr 2026 17:21:54 -0400 Subject: [PATCH 3/3] fix(install): close wget HTTPS-downgrade gap with --max-redirect=0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor bugbot flagged that wget's `--https-only` only applies to recursive downloads — for the single-file fetches in this script it's effectively a no-op, so a MITM could theoretically redirect us to an http:// mirror. curl's `--proto '=https'` closed this correctly; wget had a gap. Swap `--https-only` for `--max-redirect=0` on both wget calls. npm's registry serves metadata and tarballs directly with no redirect (verified with `curl -sI`), so this is safe for the happy path and hard-blocks any attempt to downgrade to http via redirect. Side benefit: the two metadata fetches (version + integrity) also got this protection. Previously the tarball was covered by the integrity check that follows, but a MITM on the metadata call could have fed us a fake version + matching fake integrity value. Now that vector is closed too. --- install.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index fcfd9bb75..ff6642805 100755 --- a/install.sh +++ b/install.sh @@ -116,13 +116,19 @@ detect_platform() { } # Fetch a URL to stdout, enforcing HTTPS. +# +# curl enforces HTTPS via `--proto '=https'`. wget's `--https-only` only +# applies to recursive downloads, so for the single-file fetches we do +# here we disable redirect following (`--max-redirect=0`) — npm's +# registry serves responses directly with no redirect, so this is safe +# AND blocks any MITM attempt to redirect us to http://. fetch_url() { local url="$1" if command -v curl &> /dev/null; then curl --proto '=https' --tlsv1.2 -fsSL "$url" elif command -v wget &> /dev/null; then - wget --https-only -qO- "$url" + wget --max-redirect=0 -qO- "$url" else error "Neither curl nor wget found on your system" echo "" @@ -134,7 +140,7 @@ fetch_url() { fi } -# Download a URL to a file, enforcing HTTPS. +# Download a URL to a file, enforcing HTTPS (see `fetch_url` comment). fetch_url_to_file() { local url="$1" local out="$2" @@ -142,7 +148,7 @@ fetch_url_to_file() { if command -v curl &> /dev/null; then curl --proto '=https' --tlsv1.2 -fsSL -o "$out" "$url" elif command -v wget &> /dev/null; then - wget --https-only -qO "$out" "$url" + wget --max-redirect=0 -qO "$out" "$url" else error "Neither curl nor wget found on your system" exit 1