From cf919560015398df6469183f07315a2d31f810d1 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Sun, 3 May 2026 15:37:57 +0800 Subject: [PATCH] fix(hooks+install): disk-reclaim Guard 3 + secrets per-line + sha256 fail-closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent shell hardening fixes from Opus Shell + Sonnet Shell audits. 1. disk-reclaim.sh Guard 3 — protect branches without upstream tracking (HIGH) File: hooks/disk-reclaim.sh:88-101 Bug: when a worktree branch has no upstream tracking ref, `git log @{u}..` exited non-zero and `unpushed=""` (empty). The check `[ -n "$unpushed" ] && [ "$unpushed" != "0" ]` evaluated FALSE, so the worktree fell through Guard 3 and was eligible for mtime-based pruning. Local-only branches with committed work were silently deleted. Fix: explicit two-branch logic. Run `git rev-parse --abbrev-ref @{u}` first; only run the unpushed-count check if upstream exists. If no upstream, log SKIP[no-upstream] and `continue` conservatively. New `worktrees_skip_unpushed` counter increments in both unpushed paths. 2. secrets-pre-guard.sh — placeholder allowlist scope-narrow (MEDIUM) File: hooks/secrets-pre-guard.sh:43-103 Bug: word "placeholder" anywhere in content disabled all secret-pattern scanning for that whole Write. Allowlist was too broad — a doc with the word "placeholder" in its prose could mask a real sk-ant- token elsewhere. Fix: replaced global early-exit with per-line awk scan. New scan_pattern() helper walks content line-by-line; each line matching a secret regex is allowed ONLY if the SAME line also matches ALLOWLIST_RE. Doc prose can no longer mask cross-line secrets. Added `dummy[_-]?(key|token|secret)` to allowlist for legitimate test fixtures. 3. lib-rust-prebuild.sh — sha256 fail-closed (HIGH supply-chain) File: install/lib-rust-prebuild.sh:75-88 Bug: when ${url}.sha256 404'd, installer printed WARNING and proceeded with unverified tarball. A compromised github release uploader could ship a malicious tarball, omit .sha256, and the installer would extract it into ~/.cargo/bin/. Fix: missing .sha256 → ERROR + abort. Path A install fails → falls back to Path B (cargo build from source). Override via KEI_ALLOW_UNVERIFIED_TARBALL=1 (visible per-call, intentional friction). Co-Authored-By: Claude Opus 4.7 (1M context) --- hooks/disk-reclaim.sh | 15 +++++-- hooks/secrets-pre-guard.sh | 79 ++++++++++++++++++------------------ install/lib-rust-prebuild.sh | 10 ++++- 3 files changed, 60 insertions(+), 44 deletions(-) diff --git a/hooks/disk-reclaim.sh b/hooks/disk-reclaim.sh index 6bb6e29..4161184 100755 --- a/hooks/disk-reclaim.sh +++ b/hooks/disk-reclaim.sh @@ -85,11 +85,18 @@ for proj_git in "$PROJECTS_ROOT"/*/.claude/worktrees "$PROJECTS_ROOT"/*/*/.claud continue fi - # Guard 3: unpushed - unpushed=$(cd "$wt" && git log @{u}.. 2>/dev/null | wc -l | tr -d ' ') - if [ -n "$unpushed" ] && [ "$unpushed" != "0" ]; then + # Guard 3: unpushed (fail-safe — skip on missing upstream) + if cd "$wt" 2>/dev/null && git rev-parse --abbrev-ref @{u} >/dev/null 2>&1; then + unpushed=$(git log @{u}.. 2>/dev/null | wc -l | tr -d ' ') + if [ -n "$unpushed" ] && [ "$unpushed" != "0" ]; then + worktrees_skip_unpushed=$((worktrees_skip_unpushed + 1)) + log " SKIP[unpushed=$unpushed] $wt" + continue + fi + else + # No upstream tracking — treat as "may have unpushed work", skip conservatively worktrees_skip_unpushed=$((worktrees_skip_unpushed + 1)) - log " SKIP[unpushed=$unpushed] $wt" + log " SKIP[no-upstream] $wt" continue fi diff --git a/hooks/secrets-pre-guard.sh b/hooks/secrets-pre-guard.sh index bd834ac..7a7b1fc 100755 --- a/hooks/secrets-pre-guard.sh +++ b/hooks/secrets-pre-guard.sh @@ -40,64 +40,65 @@ CONTENT=$(printf '%s' "$INPUT" | jq -r \ [ -z "$CONTENT" ] && exit 0 -# --- Allowlist: placeholder or documentation patterns ---------------------- -# If the content indicates example/placeholder values, skip. -if printf '%s' "$CONTENT" | grep -qiE \ - 'YOUR_TOKEN_HERE||\[VERIFY:|placeholder|xxx+|_TOKEN_NAME_HERE|_KEY_HERE|_SECRET_HERE|example[_-]?(key|token|secret)'; then - exit 0 -fi +# --- Per-line allowlist + secret detection --------------------------------- +# Evaluate placeholder allowlist PER LINE (not globally) so a "placeholder" +# marker elsewhere in the file does not disable secret scanning on lines +# that contain real tokens. +# +# A line is allowed iff it contains BOTH a secret-shaped pattern AND a +# placeholder marker on the SAME LINE. Otherwise, the secret pattern on +# that line is treated as a real hit. -# --- Secret detection patterns ------------------------------------------- -# Each pattern is checked individually so we can name the type in the error. +ALLOWLIST_RE='YOUR_TOKEN_HERE||\[VERIFY:|placeholder|xxx+|_TOKEN_NAME_HERE|_KEY_HERE|_SECRET_HERE|example[_-]?(key|token|secret)|dummy[_-]?(key|token|secret)' DETECTED="" +# Helper: scan content line-by-line for a given regex; for each match, +# allow only if the SAME LINE matches ALLOWLIST_RE. Sets DETECTED to label +# on first non-allowlisted hit. +scan_pattern() { + pattern="$1" + label="$2" + [ -n "$DETECTED" ] && return 0 + hit=$(printf '%s' "$CONTENT" | awk -v pat="$pattern" -v allow="$ALLOWLIST_RE" ' + { + if (match($0, pat)) { + if (match($0, allow)) { + next + } + print "HIT" + exit + } + } + ') + if [ "$hit" = "HIT" ]; then + DETECTED="$label" + fi +} + # Anthropic/OpenAI legacy key -if printf '%s' "$CONTENT" | grep -qE 'sk-[A-Za-z0-9]{20,}'; then - DETECTED="Anthropic/OpenAI legacy key (sk-...)" -fi +scan_pattern 'sk-[A-Za-z0-9]{20,}' "Anthropic/OpenAI legacy key (sk-...)" # Anthropic current key -if [ -z "$DETECTED" ] && \ - printf '%s' "$CONTENT" | grep -qE 'sk-ant-[A-Za-z0-9_-]{40,}'; then - DETECTED="Anthropic current key (sk-ant-...)" -fi +scan_pattern 'sk-ant-[A-Za-z0-9_-]{40,}' "Anthropic current key (sk-ant-...)" # GitHub classic PAT -if [ -z "$DETECTED" ] && \ - printf '%s' "$CONTENT" | grep -qE 'ghp_[A-Za-z0-9]{36}'; then - DETECTED="GitHub classic PAT (ghp_...)" -fi +scan_pattern 'ghp_[A-Za-z0-9]{36}' "GitHub classic PAT (ghp_...)" # GitHub fine-grained PAT -if [ -z "$DETECTED" ] && \ - printf '%s' "$CONTENT" | grep -qE 'github_pat_[A-Za-z0-9_]{82}'; then - DETECTED="GitHub fine-grained PAT (github_pat_...)" -fi +scan_pattern 'github_pat_[A-Za-z0-9_]{82}' "GitHub fine-grained PAT (github_pat_...)" # Slack bot token -if [ -z "$DETECTED" ] && \ - printf '%s' "$CONTENT" | grep -qE 'xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+'; then - DETECTED="Slack bot token (xoxb-...)" -fi +scan_pattern 'xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+' "Slack bot token (xoxb-...)" # Telegram bot token -if [ -z "$DETECTED" ] && \ - printf '%s' "$CONTENT" | grep -qE '[0-9]{8,10}:[A-Za-z0-9_-]{35}'; then - DETECTED="Telegram bot token (NNNNNNNNN:...)" -fi +scan_pattern '[0-9]{8,10}:[A-Za-z0-9_-]{35}' "Telegram bot token (NNNNNNNNN:...)" # AWS access key -if [ -z "$DETECTED" ] && \ - printf '%s' "$CONTENT" | grep -qE 'AKIA[A-Z0-9]{16}'; then - DETECTED="AWS access key (AKIA...)" -fi +scan_pattern 'AKIA[A-Z0-9]{16}' "AWS access key (AKIA...)" # PEM private key block -if [ -z "$DETECTED" ] && \ - printf '%s' "$CONTENT" | grep -qE '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'; then - DETECTED="PEM private key (-----BEGIN ... PRIVATE KEY-----)" -fi +scan_pattern '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----' "PEM private key (-----BEGIN ... PRIVATE KEY-----)" [ -z "$DETECTED" ] && exit 0 diff --git a/install/lib-rust-prebuild.sh b/install/lib-rust-prebuild.sh index 045876f..ebab772 100644 --- a/install/lib-rust-prebuild.sh +++ b/install/lib-rust-prebuild.sh @@ -76,7 +76,15 @@ download_release_tarball() { (cd "$tmp" && shasum -a 256 -c "${tarball}.sha256" >/dev/null 2>&1) \ || { say " sha256 mismatch on ${tarball} — refusing to install"; rm -rf "$tmp"; return 1; } else - say " WARNING: no sha256 available for ${tarball}, proceeding without verification" + say " ERROR: no sha256 sidecar found at ${url}.sha256" + say " Refusing to install unverified tarball (RULE 0.1 supply-chain hardening)." + say " Override with KEI_ALLOW_UNVERIFIED_TARBALL=1 (visible per-call)." + if [ "${KEI_ALLOW_UNVERIFIED_TARBALL:-0}" = "1" ]; then + say " KEI_ALLOW_UNVERIFIED_TARBALL=1 set — proceeding without verification (DANGEROUS)." + else + rm -rf "$tmp" + return 1 + fi fi local dst="$KIT_DIR/_primitives/_rust/target/release" mkdir -p "$dst" || { rm -rf "$tmp"; return 1; }