From a2ac1becb22049cd9fed1b062fa0386cb679997b Mon Sep 17 00:00:00 2001 From: Daniel Sierra Date: Wed, 25 Mar 2026 22:08:15 +0000 Subject: [PATCH 1/2] fix: replace unverified runtime downloads with pinned, verified installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dockerfile: pin xurl to v1.0.3 with SHA256 verification for amd64/arm64 - Dockerfile: pre-install Claude CLI v2.1.83 via npm at build time (eliminates curl|bash at runtime) - Dockerfile: pin @googleworkspace/cli to v0.22.1 - init.d/01-tools.sh: remove curl|bash Claude CLI install (no-op since pre-baked); pin GWS skills to commit a52d297 - installer/gum.sh: pin gum to v0.17.0 with SHA256 verification for Linux and macOS (x86_64/arm64) Resolves #45 — security audit 2026-03-25 Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 32 ++++++++++++++++------- init.d/01-tools.sh | 11 ++++---- installer/gum.sh | 63 ++++++++++++++++++++++++++++++---------------- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9360f67..9d14209 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,15 +27,29 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ && apt-get update && apt-get install -y gh \ && rm -rf /var/lib/apt/lists/* -# xurl CLI (X/Twitter API v2) -RUN XURL_TAG=$(curl -sf https://api.github.com/repos/xdevplatform/xurl/releases/latest | jq -r '.tag_name') \ - && [ "$XURL_TAG" != "null" ] && [ -n "$XURL_TAG" ] \ - && curl -fsSL "https://github.com/xdevplatform/xurl/releases/download/${XURL_TAG}/xurl_Linux_x86_64.tar.gz" \ - | tar -xz -C /usr/local/bin xurl \ - && chmod +x /usr/local/bin/xurl - -# Google Workspace CLI (gws) + agent skills -RUN npm install -g @googleworkspace/cli +# xurl CLI (X/Twitter API v2) — pinned version with SHA256 verification +ARG XURL_VERSION=1.0.3 +ARG XURL_SHA256_AMD64=34bc67bfbaf29ae121f7788fbd2491d3a8b95cb3947333ad39732e694497c182 +ARG XURL_SHA256_ARM64=3b56605e66508d7bc77c36cc711d41307b4cd76aec09111890b33f9d82975483 +RUN ARCH=$(dpkg --print-architecture) \ + && case "$ARCH" in \ + amd64) XURL_ARCH="x86_64"; XURL_SHA256="${XURL_SHA256_AMD64}" ;; \ + arm64) XURL_ARCH="arm64"; XURL_SHA256="${XURL_SHA256_ARM64}" ;; \ + *) echo "Unsupported architecture: $ARCH" && exit 1 ;; \ + esac \ + && curl -fsSL "https://github.com/xdevplatform/xurl/releases/download/v${XURL_VERSION}/xurl_Linux_${XURL_ARCH}.tar.gz" \ + -o /tmp/xurl.tar.gz \ + && echo "${XURL_SHA256} /tmp/xurl.tar.gz" | sha256sum -c - \ + && tar -xz -C /usr/local/bin -f /tmp/xurl.tar.gz xurl \ + && chmod +x /usr/local/bin/xurl \ + && rm /tmp/xurl.tar.gz + +# Google Workspace CLI (gws) + agent skills — pinned version +RUN npm install -g @googleworkspace/cli@0.22.1 + +# Claude CLI — pre-installed at build time, pinned version +ARG CLAUDE_CLI_VERSION=2.1.83 +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CLI_VERSION} # Agent templates (read-only source for entrypoint to copy into workspace) COPY --chown=node:node agents/ /opt/openclaw-agents/ diff --git a/init.d/01-tools.sh b/init.d/01-tools.sh index 268f466..2d90d7e 100755 --- a/init.d/01-tools.sh +++ b/init.d/01-tools.sh @@ -4,20 +4,21 @@ # Runs as `node` user. Tools are installed once and persist across restarts. # --- Claude Code CLI --- +# Pre-installed in the Docker image at build time; this is a no-op check. if command -v claude &>/dev/null; then log "Claude CLI already installed: $(claude --version 2>/dev/null || echo 'unknown')" else - log "Installing Claude CLI..." - curl -fsSL https://claude.ai/install.sh | bash - log "Claude CLI installed: $(claude --version 2>/dev/null || echo 'unknown')" + log "WARNING: Claude CLI not found. It should be pre-installed in the Docker image." fi # --- Google Workspace CLI skills --- +# Pinned to a specific commit to prevent supply-chain attacks. +GWS_COMMIT="a52d297cdfafbc53dfed66a3721a9bbd1d50dc31" if npx skills list 2>/dev/null | grep -q googleworkspace; then log "GWS skills already installed." else - log "Installing Google Workspace skills..." - npx -y skills add https://github.com/googleworkspace/cli -y + log "Installing Google Workspace skills (pinned commit: ${GWS_COMMIT})..." + npx -y skills add "https://github.com/googleworkspace/cli#${GWS_COMMIT}" -y log "GWS skills installed." fi diff --git a/installer/gum.sh b/installer/gum.sh index 6e81aaf..166a504 100644 --- a/installer/gum.sh +++ b/installer/gum.sh @@ -5,6 +5,14 @@ GUM_MIN_MAJOR=0 GUM_MIN_MINOR=14 +# Pinned release — update SHA256 values when bumping version +GUM_PINNED_VERSION="0.17.0" +# SHA256 checksums from https://github.com/charmbracelet/gum/releases/download/v0.17.0/checksums.txt +GUM_SHA256_LINUX_X86_64="69ee169bd6387331928864e94d47ed01ef649fbfe875baed1bbf27b5377a6fdb" +GUM_SHA256_LINUX_ARM64="b0b9ed95cbf7c8b7073f17b9591811f5c001e33c7cfd066ca83ce8a07c576f9c" +GUM_SHA256_DARWIN_ARM64="e2a4b8596efa05821d8c58d0c1afbcd7ad1699ba69c689cc3ff23a4a99c8b237" +GUM_SHA256_DARWIN_X86_64="cd66576aeebe6cd19c771863c7e8d696e0e1d5387d1e7075666baa67c2052e53" + # gum_detect — returns 0 if gum >= 0.14 is installed, 1 otherwise gum_detect() { if ! command -v gum >/dev/null 2>&1; then @@ -86,37 +94,50 @@ gum_install() { fi fi - # Binary download fallback - echo "Downloading gum binary from GitHub releases..." - local os arch download_url tmpdir + # Binary download — pinned version with SHA256 verification + echo "Downloading gum v${GUM_PINNED_VERSION} binary from GitHub releases..." + local os arch download_url expected_sha256 tmpdir os=$(uname -s | tr '[:upper:]' '[:lower:]') arch=$(uname -m) - case "$arch" in - x86_64) arch="x86_64" ;; - aarch64|arm64) arch="arm64" ;; + + local os_cap + os_cap=$(echo "$os" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') + + case "${os}/${arch}" in + linux/x86_64) arch="x86_64"; expected_sha256="${GUM_SHA256_LINUX_X86_64}" ;; + linux/aarch64|\ + linux/arm64) arch="arm64"; expected_sha256="${GUM_SHA256_LINUX_ARM64}" ;; + darwin/arm64) arch="arm64"; expected_sha256="${GUM_SHA256_DARWIN_ARM64}" ;; + darwin/x86_64) arch="x86_64"; expected_sha256="${GUM_SHA256_DARWIN_X86_64}" ;; *) - echo "Unsupported architecture: $arch" + echo "Unsupported platform: ${os}/${arch}" return 1 ;; esac - # Query latest release from GitHub API - local version - version=$(curl -sf "https://api.github.com/repos/charmbracelet/gum/releases/latest" 2>/dev/null | \ - grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 | tr -d 'v' || echo "") - if [ -z "$version" ]; then - # Fallback version if API is unreachable - version="0.14.5" - echo "Could not fetch latest version, trying $version..." - fi - - local os_cap - os_cap=$(echo "$os" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') - - download_url="https://github.com/charmbracelet/gum/releases/download/v${version}/gum_${version}_${os_cap}_${arch}.tar.gz" + download_url="https://github.com/charmbracelet/gum/releases/download/v${GUM_PINNED_VERSION}/gum_${GUM_PINNED_VERSION}_${os_cap}_${arch}.tar.gz" tmpdir=$(mktemp -d) if curl -fsSL "$download_url" -o "${tmpdir}/gum.tar.gz" 2>/dev/null; then + # Verify SHA256 checksum before extracting + local actual_sha256 + if command -v sha256sum >/dev/null 2>&1; then + actual_sha256=$(sha256sum "${tmpdir}/gum.tar.gz" | cut -d' ' -f1) + elif command -v shasum >/dev/null 2>&1; then + actual_sha256=$(shasum -a 256 "${tmpdir}/gum.tar.gz" | cut -d' ' -f1) + else + echo "ERROR: No sha256sum or shasum found; cannot verify download." + rm -rf "$tmpdir" + return 1 + fi + if [ "$actual_sha256" != "$expected_sha256" ]; then + echo "ERROR: SHA256 mismatch for gum download." + echo " Expected: ${expected_sha256}" + echo " Got: ${actual_sha256}" + rm -rf "$tmpdir" + return 1 + fi + tar -xzf "${tmpdir}/gum.tar.gz" -C "$tmpdir" 2>/dev/null local gum_bin gum_bin=$(find "$tmpdir" -name "gum" -type f | head -1) From 86ef77728ffb9bad05afe2bb97f8b90ffc0d587b Mon Sep 17 00:00:00 2001 From: Daniel Sierra Ramos Date: Thu, 26 Mar 2026 11:35:40 +0100 Subject: [PATCH 2/2] fix: merge npm layers, simplify boot check, align SHA256 verification - Merge two npm install -g layers into one (faster builds, better cache) - Simplify Claude CLI boot check to one-liner (no more --version spawning Node) - Move GWS_COMMIT to top of 01-tools.sh with cross-reference comment - Align gum.sh SHA256 verification to use sha256sum -c - pattern - Remove narrating comments, add meaningful why-comment Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile | 9 ++++----- init.d/01-tools.sh | 14 ++++---------- installer/gum.sh | 13 +++++-------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index dc646c9..b4fde02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,12 +45,11 @@ RUN ARCH=$(dpkg --print-architecture) \ && chmod +x /usr/local/bin/xurl \ && rm /tmp/xurl.tar.gz -# Google Workspace CLI (gws) + agent skills — pinned version -RUN npm install -g @googleworkspace/cli@0.22.1 - -# Claude CLI — pre-installed at build time, pinned version +# Google Workspace CLI + Claude CLI — pinned versions, single layer ARG CLAUDE_CLI_VERSION=2.1.83 -RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CLI_VERSION} +RUN npm install -g \ + @googleworkspace/cli@0.22.1 \ + @anthropic-ai/claude-code@${CLAUDE_CLI_VERSION} # Agent templates (read-only source for entrypoint to copy into workspace) COPY --chown=node:node agents/ /opt/openclaw-agents/ diff --git a/init.d/01-tools.sh b/init.d/01-tools.sh index 2d90d7e..41d8946 100755 --- a/init.d/01-tools.sh +++ b/init.d/01-tools.sh @@ -3,17 +3,11 @@ # SCRIPT_NAME and log() are provided by docker-entrypoint.sh # Runs as `node` user. Tools are installed once and persist across restarts. -# --- Claude Code CLI --- -# Pre-installed in the Docker image at build time; this is a no-op check. -if command -v claude &>/dev/null; then - log "Claude CLI already installed: $(claude --version 2>/dev/null || echo 'unknown')" -else - log "WARNING: Claude CLI not found. It should be pre-installed in the Docker image." -fi +# Pinned versions (correspond to Dockerfile npm pins) +GWS_COMMIT="a52d297cdfafbc53dfed66a3721a9bbd1d50dc31" # @googleworkspace/cli@0.22.1 -# --- Google Workspace CLI skills --- -# Pinned to a specific commit to prevent supply-chain attacks. -GWS_COMMIT="a52d297cdfafbc53dfed66a3721a9bbd1d50dc31" +# Claude CLI is pre-installed in the Docker image; assert it is on PATH. +command -v claude >/dev/null 2>&1 || log "WARNING: Claude CLI not found in image." if npx skills list 2>/dev/null | grep -q googleworkspace; then log "GWS skills already installed." else diff --git a/installer/gum.sh b/installer/gum.sh index 166a504..8b7b075 100644 --- a/installer/gum.sh +++ b/installer/gum.sh @@ -94,7 +94,6 @@ gum_install() { fi fi - # Binary download — pinned version with SHA256 verification echo "Downloading gum v${GUM_PINNED_VERSION} binary from GitHub releases..." local os arch download_url expected_sha256 tmpdir os=$(uname -s | tr '[:upper:]' '[:lower:]') @@ -119,21 +118,19 @@ gum_install() { tmpdir=$(mktemp -d) if curl -fsSL "$download_url" -o "${tmpdir}/gum.tar.gz" 2>/dev/null; then - # Verify SHA256 checksum before extracting - local actual_sha256 + # Verify integrity before extracting (tampered archive could exploit tar path traversal) + local sha_cmd if command -v sha256sum >/dev/null 2>&1; then - actual_sha256=$(sha256sum "${tmpdir}/gum.tar.gz" | cut -d' ' -f1) + sha_cmd="sha256sum" elif command -v shasum >/dev/null 2>&1; then - actual_sha256=$(shasum -a 256 "${tmpdir}/gum.tar.gz" | cut -d' ' -f1) + sha_cmd="shasum -a 256" else echo "ERROR: No sha256sum or shasum found; cannot verify download." rm -rf "$tmpdir" return 1 fi - if [ "$actual_sha256" != "$expected_sha256" ]; then + if ! echo "${expected_sha256} ${tmpdir}/gum.tar.gz" | $sha_cmd -c - >/dev/null 2>&1; then echo "ERROR: SHA256 mismatch for gum download." - echo " Expected: ${expected_sha256}" - echo " Got: ${actual_sha256}" rm -rf "$tmpdir" return 1 fi