fix(hooks+install): disk-reclaim Guard 3 + secrets per-line + sha256 fail-closed
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) <noreply@anthropic.com>
This commit is contained in:
parent
97ffa5b4dc
commit
cf91956001
3 changed files with 60 additions and 44 deletions
|
|
@ -85,11 +85,18 @@ for proj_git in "$PROJECTS_ROOT"/*/.claude/worktrees "$PROJECTS_ROOT"/*/*/.claud
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Guard 3: unpushed
|
# Guard 3: unpushed (fail-safe — skip on missing upstream)
|
||||||
unpushed=$(cd "$wt" && git log @{u}.. 2>/dev/null | wc -l | tr -d ' ')
|
if cd "$wt" 2>/dev/null && git rev-parse --abbrev-ref @{u} >/dev/null 2>&1; then
|
||||||
if [ -n "$unpushed" ] && [ "$unpushed" != "0" ]; 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))
|
worktrees_skip_unpushed=$((worktrees_skip_unpushed + 1))
|
||||||
log " SKIP[unpushed=$unpushed] $wt"
|
log " SKIP[no-upstream] $wt"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,64 +40,65 @@ CONTENT=$(printf '%s' "$INPUT" | jq -r \
|
||||||
|
|
||||||
[ -z "$CONTENT" ] && exit 0
|
[ -z "$CONTENT" ] && exit 0
|
||||||
|
|
||||||
# --- Allowlist: placeholder or documentation patterns ----------------------
|
# --- Per-line allowlist + secret detection ---------------------------------
|
||||||
# If the content indicates example/placeholder values, skip.
|
# Evaluate placeholder allowlist PER LINE (not globally) so a "placeholder"
|
||||||
if printf '%s' "$CONTENT" | grep -qiE \
|
# marker elsewhere in the file does not disable secret scanning on lines
|
||||||
'YOUR_TOKEN_HERE|<redacted>|\[VERIFY:|placeholder|xxx+|_TOKEN_NAME_HERE|_KEY_HERE|_SECRET_HERE|example[_-]?(key|token|secret)'; then
|
# that contain real tokens.
|
||||||
exit 0
|
#
|
||||||
fi
|
# 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 -------------------------------------------
|
ALLOWLIST_RE='YOUR_TOKEN_HERE|<redacted>|\[VERIFY:|placeholder|xxx+|_TOKEN_NAME_HERE|_KEY_HERE|_SECRET_HERE|example[_-]?(key|token|secret)|dummy[_-]?(key|token|secret)'
|
||||||
# Each pattern is checked individually so we can name the type in the error.
|
|
||||||
|
|
||||||
DETECTED=""
|
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
|
# Anthropic/OpenAI legacy key
|
||||||
if printf '%s' "$CONTENT" | grep -qE 'sk-[A-Za-z0-9]{20,}'; then
|
scan_pattern 'sk-[A-Za-z0-9]{20,}' "Anthropic/OpenAI legacy key (sk-...)"
|
||||||
DETECTED="Anthropic/OpenAI legacy key (sk-...)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Anthropic current key
|
# Anthropic current key
|
||||||
if [ -z "$DETECTED" ] && \
|
scan_pattern 'sk-ant-[A-Za-z0-9_-]{40,}' "Anthropic current key (sk-ant-...)"
|
||||||
printf '%s' "$CONTENT" | grep -qE 'sk-ant-[A-Za-z0-9_-]{40,}'; then
|
|
||||||
DETECTED="Anthropic current key (sk-ant-...)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# GitHub classic PAT
|
# GitHub classic PAT
|
||||||
if [ -z "$DETECTED" ] && \
|
scan_pattern 'ghp_[A-Za-z0-9]{36}' "GitHub classic PAT (ghp_...)"
|
||||||
printf '%s' "$CONTENT" | grep -qE 'ghp_[A-Za-z0-9]{36}'; then
|
|
||||||
DETECTED="GitHub classic PAT (ghp_...)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# GitHub fine-grained PAT
|
# GitHub fine-grained PAT
|
||||||
if [ -z "$DETECTED" ] && \
|
scan_pattern 'github_pat_[A-Za-z0-9_]{82}' "GitHub fine-grained PAT (github_pat_...)"
|
||||||
printf '%s' "$CONTENT" | grep -qE 'github_pat_[A-Za-z0-9_]{82}'; then
|
|
||||||
DETECTED="GitHub fine-grained PAT (github_pat_...)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Slack bot token
|
# Slack bot token
|
||||||
if [ -z "$DETECTED" ] && \
|
scan_pattern 'xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+' "Slack bot token (xoxb-...)"
|
||||||
printf '%s' "$CONTENT" | grep -qE 'xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+'; then
|
|
||||||
DETECTED="Slack bot token (xoxb-...)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Telegram bot token
|
# Telegram bot token
|
||||||
if [ -z "$DETECTED" ] && \
|
scan_pattern '[0-9]{8,10}:[A-Za-z0-9_-]{35}' "Telegram bot token (NNNNNNNNN:...)"
|
||||||
printf '%s' "$CONTENT" | grep -qE '[0-9]{8,10}:[A-Za-z0-9_-]{35}'; then
|
|
||||||
DETECTED="Telegram bot token (NNNNNNNNN:...)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# AWS access key
|
# AWS access key
|
||||||
if [ -z "$DETECTED" ] && \
|
scan_pattern 'AKIA[A-Z0-9]{16}' "AWS access key (AKIA...)"
|
||||||
printf '%s' "$CONTENT" | grep -qE 'AKIA[A-Z0-9]{16}'; then
|
|
||||||
DETECTED="AWS access key (AKIA...)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PEM private key block
|
# PEM private key block
|
||||||
if [ -z "$DETECTED" ] && \
|
scan_pattern '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----' "PEM private key (-----BEGIN ... PRIVATE KEY-----)"
|
||||||
printf '%s' "$CONTENT" | grep -qE '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'; then
|
|
||||||
DETECTED="PEM private key (-----BEGIN ... PRIVATE KEY-----)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -z "$DETECTED" ] && exit 0
|
[ -z "$DETECTED" ] && exit 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,15 @@ download_release_tarball() {
|
||||||
(cd "$tmp" && shasum -a 256 -c "${tarball}.sha256" >/dev/null 2>&1) \
|
(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; }
|
|| { say " sha256 mismatch on ${tarball} — refusing to install"; rm -rf "$tmp"; return 1; }
|
||||||
else
|
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
|
fi
|
||||||
local dst="$KIT_DIR/_primitives/_rust/target/release"
|
local dst="$KIT_DIR/_primitives/_rust/target/release"
|
||||||
mkdir -p "$dst" || { rm -rf "$tmp"; return 1; }
|
mkdir -p "$dst" || { rm -rf "$tmp"; return 1; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue