fix(security): bash-guard git-ops check inspects commands, not file content (#165)

The git network/auth deny rule matched its regex against the whole
command string, so heredoc bodies and echo/printf arguments that merely
documented git verbs (a README, a notes file) were treated as git
invocations and denied. This wedged smoke-13's dev: after wiping the
README via an Edit/Write fallback it could not restore it because every
`cat > README.md << EOF ... git commit ... EOF` was blocked.

The git-ops check now runs against a skeleton of the command with
heredoc bodies and echo/printf literal args stripped (both are data the
shell writes, never executed). Quoted args to a shell interpreter
(`bash -c "... && git fetch"`) ARE executed, are not echo/printf/heredoc
bodies, and so survive untouched — the hook's core purpose is preserved.
A sentinel prefix distinguishes a legitimately-empty skeleton from a
python failure (fail closed on failure). All other rules, including the
#164 import-bypass rule, still inspect the full command.
This commit is contained in:
Renn F
2026-05-16 02:20:08 +02:00
parent 81f5655d48
commit 1605d187f1
2 changed files with 125 additions and 1 deletions
+46 -1
View File
@@ -36,8 +36,53 @@ except Exception:
low=$(printf '%s' "$cmd" | tr "[:upper:]" "[:lower:]")
# Skeletonize the command for the git-ops check ONLY (#165): strip heredoc
# bodies and echo/printf literal arguments. Those are data the shell writes
# to a file, never commands the shell executes — so a README/heredoc that
# merely documents `git commit` must not be mistaken for invoking git.
# Quoted args to a shell interpreter (`bash -c "... && git fetch"`) ARE
# executed, are not echo/printf/heredoc bodies, and so survive untouched.
# Every other rule below still inspects the full command ($low).
git_skel=$(printf '%s' "$cmd" | python3 -c '
import sys, re
src = sys.stdin.read()
lines = src.split("\n")
opener = re.compile(r"<<-?\s*[^\sA-Za-z_]*([A-Za-z_]\w*)")
kept = []
i = 0
n = len(lines)
while i < n:
line = lines[i]
kept.append(line)
m = opener.search(line)
if m:
delim = m.group(1)
dash = "<<-" in line
i += 1
while i < n:
body = lines[i]
cand = body.strip() if dash else body
if cand == delim:
kept.append(body)
break
i += 1
i += 1
skel = "\n".join(kept)
skel = re.sub(r"(^|[\n;&|]|&&|\|\|)\s*(echo|printf)\b[^\n;&|]*", r"\1", skel)
sys.stdout.write("__SKEL_OK__" + skel)
' 2>/dev/null)
# A successful run is prefixed with the sentinel even when the skeleton is
# legitimately empty (whole command was echo/heredoc). No sentinel means
# python failed — fail closed by inspecting the full command.
if [[ "$git_skel" == __SKEL_OK__* ]]; then
git_skel="${git_skel#__SKEL_OK__}"
else
git_skel="$cmd"
fi
git_skel_low=$(printf '%s' "$git_skel" | tr "[:upper:]" "[:lower:]")
# --- git network / auth ops ---------------------------------------------------
if echo "$low" | grep -qE '(^|[[:space:];&|])git[[:space:]]+(fetch|pull|push|clone|remote|ls-remote|checkout|commit|merge|rebase|reset|cherry-pick|revert|tag[[:space:]]+-d|update-ref|reflog[[:space:]]+delete)'; then
if echo "$git_skel_low" | grep -qE '(^|[[:space:];&|])git[[:space:]]+(fetch|pull|push|clone|remote|ls-remote|checkout|commit|merge|rebase|reset|cherry-pick|revert|tag[[:space:]]+-d|update-ref|reflog[[:space:]]+delete)'; then
echo "Denied: shell git for network / auth / branch-mutating ops is blocked." >&2
echo "Use the verb listed in your role's State→Verb table (e.g. commit, complete, i_am_done)." >&2
exit 2