From bfa1011d20d319de9e938b4f2e50790fe0146c94 Mon Sep 17 00:00:00 2001 From: antony Date: Sun, 5 Jul 2026 18:44:11 +1000 Subject: [PATCH] =?UTF-8?q?fix(security):=20resolve=20hook=20dir=20via=20g?= =?UTF-8?q?it,=20assert=20install=20landed=20=E2=80=94=20close=20audit=202?= =?UTF-8?q?e-02?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPT-5.5 re-audit finding 2e-02 (follow-on to 2e-01). The installer hand-parsed core.hooksPath with 'case $cur in [A-Za-z]:*) HOOKDIR=$cur', using a Windows-native absolute path (backslashes) verbatim in bash. That depends on MSYS backslash-path translation, which is not portable across bash-on-Windows variants — in some setups it lands the hardened hook where git never reads it while the OLD (ungated) hook stays active, i.e. a false 'installed' that leaves the RCE open. (Note: the specific mangling did NOT reproduce under this box's Git Bash — the path translated correctly both ways — but the dependency is real and environment-specific, so it is removed rather than argued.) Fix: - Resolve the active hooks dir the way git itself does: 'git rev-parse --path-format=absolute --git-path hooks' — honors core.hooksPath and returns an absolute FORWARD-SLASH path usable in every bash-on-Windows variant. We now write exactly where git reads. Fallback for git<2.31 normalizes via cygpath/tr. - POST-INSTALL ASSERTION: after writing, confirm the gate block is present at git's resolved active hook path; if not, print ERROR and exit 1 instead of reporting success. A misplaced write can no longer masquerade as a successful, protective install. - Refresh/chain/create paths unified through one success+assert tail; husky (sourced, no shebang) detected by path. Verified: default, Windows-backslash core.hooksPath, and custom-relative dir all land the gate at git's resolved path; re-run refreshes to a single block (idempotent); a forced misplaced write triggers the ERROR/exit-1 assertion. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CsePmfHDMBKRNDLNb3TLD5 --- tools/install-gate.sh | 54 ++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/tools/install-gate.sh b/tools/install-gate.sh index 250ab7f..ad8572d 100644 --- a/tools/install-gate.sh +++ b/tools/install-gate.sh @@ -43,16 +43,24 @@ PIN="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || git } > "$PIN" echo "[verax-gate] pinned gate integrity → $PIN" -# Pick the ACTIVE hook directory (do NOT change it). +# Resolve the ACTIVE hook directory the way GIT ITSELF does — respecting core.hooksPath — as an +# absolute, FORWARD-SLASH path. We must write exactly where git reads: hand-parsing a Windows-native +# `core.hooksPath` (backslashes) is not portable across bash-on-Windows variants (Git Bash vs WSL vs +# pathconv-disabled) and could land the hardened hook somewhere git never reads while the OLD vulnerable +# hook stays active — a false "installed" that leaves the RCE open (GPT-5.5 audit 2e-02). `git rev-parse +# --git-path hooks` honors core.hooksPath; `--path-format=absolute` normalizes slashes for the shell. cur="$(git config --get core.hooksPath || true)" -if [ -d "$ROOT/.husky" ]; then - HOOKDIR="$ROOT/.husky"; KIND="husky" -elif [ -n "$cur" ]; then - case "$cur" in /*|[A-Za-z]:*) HOOKDIR="$cur" ;; *) HOOKDIR="$ROOT/$cur" ;; esac # relative → repo-rooted - KIND="custom ($cur)" -else - HOOKDIR="$ROOT/.git/hooks"; KIND="default" +HOOKDIR="$(git rev-parse --path-format=absolute --git-path hooks 2>/dev/null || true)" +if [ -z "$HOOKDIR" ]; then + # Fallback for git < 2.31 (no --path-format): normalize the raw value deterministically. + if [ -n "$cur" ]; then + HOOKDIR="$(cygpath -u "$cur" 2>/dev/null || printf '%s' "$cur" | tr '\\' '/')" + case "$HOOKDIR" in /*|[A-Za-z]:/*) ;; *) HOOKDIR="$ROOT/$HOOKDIR" ;; esac # relative → repo-rooted + else + HOOKDIR="$ROOT/.git/hooks" + fi fi +[ -n "$cur" ] && KIND="custom ($cur)" || KIND="default" mkdir -p "$HOOKDIR" HOOK="$HOOKDIR/pre-push" MARK="# >>> verax local gate >>>" @@ -101,21 +109,29 @@ if [ -f "$HOOK" ] && grep -qF "$MARK" "$HOOK"; then # Re-pin (done above) but refresh the block too, so upgrades to this installer take effect. tmp="$(mktemp)"; awk -v m="$MARK" -v e="$END" '$0==m{s=1} !s{print} $0==e{s=0}' "$HOOK" > "$tmp" && mv "$tmp" "$HOOK" { echo ""; echo "$MARK"; echo "$DELEGATE"; echo "$END"; } >> "$HOOK" - chmod +x "$HOOK" 2>/dev/null || true - echo "[verax-gate] refreshed the gate block in the $KIND hook ($HOOK)." - echo " gate spec: .verax/gate.json (integrity-pinned) · bypass: git push --no-verify (or VERAX_SKIP_GATE=1)" - exit 0 -fi - -if [ -f "$HOOK" ]; then + ACTION="refreshed the gate block in" +elif [ -f "$HOOK" ]; then # CHAIN: keep the existing pre-push, append our verified-delegate block. { echo ""; echo "$MARK"; echo "$DELEGATE"; echo "$END"; } >> "$HOOK" - echo "[verax-gate] CHAINED the gate onto the existing $KIND pre-push ($HOOK)." + ACTION="CHAINED the gate onto the existing" else - # No existing pre-push: create one (with a shebang for non-husky dirs). - { [ "$KIND" = husky ] || echo "#!/bin/bash"; echo "$MARK"; echo "$DELEGATE"; echo "$END"; } > "$HOOK" - echo "[verax-gate] installed a new $KIND pre-push ($HOOK)." + # No existing pre-push: create one. Husky sources its hooks, so it needs no shebang; others do. + case "$HOOKDIR" in *.husky*) : ;; *) printf '#!/bin/bash\n' > "$HOOK" ;; esac + { echo "$MARK"; echo "$DELEGATE"; echo "$END"; } >> "$HOOK" + ACTION="installed a new" fi chmod +x "$HOOK" 2>/dev/null || true + +# POST-INSTALL ASSERTION: confirm the gate is present at the hook path GIT WILL ACTUALLY USE. Because +# HOOKDIR is git's own resolved hooks path, this holds by construction — but we verify rather than assume, +# so a path-resolution quirk can never leave a false "installed" while the OLD (ungated) hook stays active. +ACTIVE="$(git rev-parse --path-format=absolute --git-path hooks 2>/dev/null || printf '%s' "$HOOKDIR")/pre-push" +if ! grep -qF "$MARK" "$ACTIVE" 2>/dev/null; then + echo "[verax-gate] ERROR: post-install check FAILED — the gate is NOT at the active hook path git will run:" + echo "[verax-gate] $ACTIVE" + echo "[verax-gate] Your pushes are NOT gated. Refusing to report success. (core.hooksPath=${cur:-})" + exit 1 +fi +echo "[verax-gate] $ACTION $KIND hook ($HOOK)." echo " gate spec: .verax/gate.json (integrity-pinned) · bypass: git push --no-verify (or VERAX_SKIP_GATE=1)" echo " remove: delete the '$MARK ... $END' block from $HOOK (and $PIN)"