voyage/tests/guard-bypass-corpus-4-5b2-a.sh

251 lines
15 KiB
Bash

#!/usr/bin/env bash
# tests/guard-bypass-corpus-4-5b2-a.sh
# The bypass corpus of fix round 4.5b2, part A (plan
# .claude/projects/2026-10-31-guard-4-5b2-part-a/plan.md, Step 3, 3c;
# decisions 91, 107 and 123): commands the args-heredoc guard in
# hooks/scripts/pre-bash-executor.mjs must block. Each entry is fed to the
# hook as a Bash tool input; it passes when the hook exits 2 and names
# args-heredoc-closed-early.
#
# guard-bypass-corpus-4-5b2-a.sh [--hook <absolute path>]
#
# --hook defaults to hooks/scripts/pre-bash-executor.mjs of this repo.
# Exit 0 every entry blocked, 1 an entry not blocked (its FAIL line names it),
# 2 a setup failure ("corpus: setup failure: <reason>": a hook path that does
# not exist, an unknown argument, no entries, a bad line, a duplicate name);
# never 0 on a setup failure.
#
# Entries: one line each, "E <name> <JSON string>", after one comment line
# "# <name>: expands under bash|zsh|both|neither (blocked conservatively);
# measured <date>". The JSON keeps every byte ASCII (CR and tab show as \r
# and \t). fs-<n> is the blocked command on line <n> of
# tests/hooks/bash-guard-false-stops.test.mjs and ag-<n> the one on line <n> of
# tests/hooks/bash-guard-args-gate.test.mjs, each with its template expanded,
# as the branch left them (fixed when written: a later edit of those files
# does not update this corpus). The other names are the plan's (its Probe
# Catalogue and Step 3, 3c).
# "expands under" was measured with /bin/bash (3.2.57) and /bin/zsh -f (5.9),
# each in its own temp directory with HOME set to a temp directory and
# PATH=/usr/bin:/bin, the entry's $(touch PWNED) rewritten to
# $(touch PWNED); touch RAN: it expands when a PWNED file exists and RAN does
# not. "neither" entries are blocked conservatively: a parse error, a quote
# left open, or a body read as text.
# Starts nothing but node and the hook. bash 3.2 compatible, ASCII only, no
# arrays.
set -u
setup_fail() {
echo "corpus: setup failure: $1" >&2
exit 2
}
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" || setup_fail "cannot find the repo root from $0"
HOOK="$ROOT/hooks/scripts/pre-bash-executor.mjs"
while [ $# -gt 0 ]; do
case "$1" in
--hook)
[ $# -ge 2 ] || setup_fail "--hook needs a path"
HOOK="$2"
shift 2
;;
*)
setup_fail "unknown argument: $1"
;;
esac
done
[ -f "$HOOK" ] || setup_fail "no hook at $HOOK"
command -v node > /dev/null 2>&1 || setup_fail "node not found"
unset CLAUDE_CODE_SESSION_ID
# The reader: parses the entries on stdin, feeds each to the hook, prints
# "ok - <name>" or "FAIL - <name>: exit <n>" and a count line. Any error of
# its own is a setup failure (exit 2).
READER='
const fs = require("fs");
const { spawnSync } = require("child_process");
const setup = (why) => {
process.stderr.write("corpus: setup failure: " + why + "\n");
process.exit(2);
};
try {
const hook = process.argv[1];
if (!hook || !fs.existsSync(hook)) setup("no hook at " + hook);
const entries = [];
const seen = new Set();
fs.readFileSync(0, "utf8").split("\n").forEach((line, i) => {
if (line === "" || line.startsWith("#")) return;
const m = /^E ([a-z0-9][a-z0-9-]*) (".*")$/.exec(line);
if (!m) setup("bad line " + (i + 1) + ": " + line.slice(0, 80));
let command;
try {
command = JSON.parse(m[2]);
} catch {
setup("bad JSON string on line " + (i + 1));
}
if (typeof command !== "string") setup("not a JSON string on line " + (i + 1));
if (seen.has(m[1])) setup("duplicate name " + m[1]);
seen.add(m[1]);
entries.push({ name: m[1], command });
});
if (entries.length === 0) setup("no entries");
let failed = 0;
for (const e of entries) {
const r = spawnSync(process.execPath, [hook], {
input: JSON.stringify({ tool_name: "Bash", tool_input: { command: e.command } }),
encoding: "utf8",
timeout: 20000,
maxBuffer: 16 * 1024 * 1024,
});
if (r.status === 2 && String(r.stderr).includes("args-heredoc-closed-early")) {
console.log("ok - " + e.name);
continue;
}
failed += 1;
if (r.error && r.error.code === "ETIMEDOUT") console.log("FAIL - " + e.name + ": timeout after 20 s");
else if (r.status === null) console.log("FAIL - " + e.name + ": exit null (signal " + r.signal + ")");
else console.log("FAIL - " + e.name + ": exit " + r.status);
}
console.log("corpus: " + entries.length + " entries, " + (entries.length - failed) + " blocked, " + failed + " not blocked");
process.exit(failed === 0 ? 0 : 1);
} catch (err) {
setup("reader error: " + (err && err.message));
}
'
cat <<'CORPUS' | node -e "$READER" "$HOOK"
# fs-75: expands under neither (blocked conservatively); measured 2026-11-02
E fs-75 "node /plugin/lib/parsers/arg-parser.mjs --command trekendsession --stdin <<\"VOYAGE_ARGS_EOF_7F3C9E\nbrief.md"
# fs-78: expands under neither (blocked conservatively); measured 2026-11-02
E fs-78 "echo \"x\" && node /plugin/lib/parsers/arg-parser.mjs --command trekendsession --stdin <<V\"OYAGE_ARGS_EOF_7F3C9E\nbrief.md\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-96: expands under both; measured 2026-11-02
E fs-96 "echo \"a<<b\" && node /plugin/lib/parsers/arg-parser.mjs --command trekendsession --stdin <<VOYAGE_ARGS\\\n_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-104: expands under both; measured 2026-11-02
E fs-104 "cat \\<<<VOYAGE_ARGS\\\n_EOF_7F3C9E\nHDBODY #$(touch PWNED1)"
# fs-105: expands under both; measured 2026-11-02
E fs-105 "cat \\<\\\n<<VOYAGE_ARGS\\\n_EOF_7F3C9E\nHDBODY #$(touch PWNED1)"
# fs-106: expands under bash; measured 2026-11-02
E fs-106 "cat \\<<\\\n<VOYAGE_ARGS_EOF_7F3C9E\nHDBODY #$(touch PWNED1)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-107: expands under both; measured 2026-11-02
E fs-107 "cat \\<<<VOYAGE_ARGS\\\n_EOF\\\n_7F3C9E\nHDBODY #$(touch PWNED1)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-108: expands under both; measured 2026-11-02
E fs-108 "cat \\<<<V\"OYAGE_ARGS_EOF_7F3C9E\"\nx\nVOYAGE_ARGS_EOF_7F3C9E\ntouch PWNED2"
# fs-112: expands under both; measured 2026-11-02
E fs-112 "echo \\\\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-114: expands under both; measured 2026-11-02
E fs-114 "echo '\\'<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-116: expands under both; measured 2026-11-02
E fs-116 "echo '\\'<<VOYAGE_ARGS\\\n_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-118: expands under neither (blocked conservatively); measured 2026-11-02
E fs-118 "echo 'x' && echo \"y \\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-120: expands under both; measured 2026-11-02
E fs-120 "echo \\<<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-122: expands under both; measured 2026-11-02
E fs-122 "echo a\\\\\n\\\\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# fs-127: expands under both; measured 2026-11-02
E fs-127 "bash -c \"cat \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# fs-129: expands under both; measured 2026-11-02
E fs-129 "x=`cat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n$(echo EXPANDED)\nVOYAGE_ARGS_EOF_7F3C9E`"
# fs-132: expands under both; measured 2026-11-02
E fs-132 "bash <<EOF\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nEOF"
# ag-61: expands under both; measured 2026-11-02
E ag-61 "node /plugin/lib/parsers/arg-parser.mjs --command trekendsession --stdin <<VOYAGE_ARGS\\\n_EOF_7F3C9E\nbrief.md $(touch PWNED)"
# ag-62: expands under both; measured 2026-11-02
E ag-62 "node /plugin/lib/parsers/arg-parser.mjs --command trekendsession --stdin <<VOYAGE_ARGS_EOF\\\n_7F3C9E\nbrief.md $(touch PWNED)"
# scan-single-backslash: expands under both; measured 2026-11-02
E scan-single-backslash "echo '\\'; bash -c \"echo \\\"'\\\"; cat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# scan-dq-escaped-quote: expands under both; measured 2026-11-02
E scan-dq-escaped-quote "bash -c \"echo \\\"x\\\"; cat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-quote-open: expands under neither (blocked conservatively); measured 2026-11-02
E esc-quote-open "echo \"y \\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# esc-bash-c-dq: expands under both; measured 2026-11-02
E esc-bash-c-dq "bash -c \"cat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-eval-dq: expands under both; measured 2026-11-02
E esc-eval-dq "eval \"cat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-bash-c-ansi: expands under bash; measured 2026-11-02
E esc-bash-c-ansi "bash -c $'cat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E'"
# esc-eval-ansi: expands under bash; measured 2026-11-02
E esc-eval-ansi "eval $'cat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E'"
# esc-bash-c-locale: expands under both; measured 2026-11-02
E esc-bash-c-locale "bash -c $\"cat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-heredoc-open: expands under both; measured 2026-11-02
E esc-heredoc-open "bash <<EOF\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# esc-backquote: expands under neither (blocked conservatively); measured 2026-11-02
E esc-backquote "x=`date`; echo \\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E"
# esc-close-trailing-space: expands under neither (blocked conservatively); measured 2026-11-02
E esc-close-trailing-space "cat <<'EOF'\nEOF \necho \\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nEOF"
# esc-close-cr: expands under neither (blocked conservatively); measured 2026-11-02
E esc-close-cr "cat <<'EOF'\nEOF\r\necho \\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nEOF"
# esc-close-tab: expands under neither (blocked conservatively); measured 2026-11-02
E esc-close-tab "cat <<'EOF'\n\tEOF\necho \\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nEOF"
# esc-two-heredocs: expands under both; measured 2026-11-02
E esc-two-heredocs "cat <<A <<B\nA\necho \\<<VOYAGE_ARGS_EOF_7F3C9E\nbrief.md $(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nB"
# esc-cj: expands under both; measured 2026-11-02
E esc-cj "echo \\<x; bash <<EOF\nx\\\nEOF\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nEOF"
# esc-e1: expands under both; measured 2026-11-02
E esc-e1 "# don't\nbash -c \"# it's\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-e2: expands under both; measured 2026-11-02
E esc-e2 "echo $((1<<X))\nbash -c \":\nX\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-e2b: expands under both; measured 2026-11-02
E esc-e2b "# see <<X\nbash -c \":\nX\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-e4b: expands under both; measured 2026-11-02
E esc-e4b "x=$(cat <<X)\nbash -c \":\nX\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-e4d: expands under both; measured 2026-11-02
E esc-e4d "cat <(cat <<X) >/dev/null\nbash -c \":\nX\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-e8: expands under both; measured 2026-11-02
E esc-e8 "bash <<X$(:)\nX$\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nX$(:)"
# esc-e3: expands under both; measured 2026-11-02
E esc-e3 "bash -s <<X \\\nX\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nX"
# esc-e18: expands under bash; measured 2026-11-02
E esc-e18 "bash -c \"$\\\n(: '\"')# '\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-e19: expands under both; measured 2026-11-02
E esc-e19 "bash -c $\\\n'# \\' \"'\"\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\""
# esc-cnt: expands under bash; measured 2026-11-02
E esc-cnt "bash <\\\n<EOF\ncat - \\\\\\<<VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\nVOYAGE_ARGS_EOF_7F3C9E\nEOF"
# sfx-word: expands under both; measured 2026-11-02
E sfx-word "cat <<VOYAGE_ARGS\\\n_EOF_7F3C9EX\nbody $(echo EXPANDED)"
# sfx-dot: expands under both; measured 2026-11-02
E sfx-dot "cat <<VOYAGE_ARGS\\\n_EOF_7F3C9E.\nbody $(echo EXPANDED)"
# sfx-backquote: expands under neither (blocked conservatively); measured 2026-11-02
E sfx-backquote "cat <<VOYAGE_ARGS\\\n_EOF_7F3C9E`\nbody $(echo EXPANDED)"
# split-in-string: expands under both; measured 2026-11-02
E split-in-string "bash -c 'cat <<VOYAGE_ARGS\\\n_EOF_7F3C9E'\"\n\\$(touch PWNED); touch RAN\n\""
# split-in-string-dot: expands under both; measured 2026-11-02
E split-in-string-dot "bash -c 'cat <<VOYAGE_ARGS\\\n_EOF_7F3C9E.'\"\n\\$(touch PWNED); touch RAN\n\""
# split-in-string-word: expands under both; measured 2026-11-02
E split-in-string-word "bash -c 'cat <<VOYAGE_ARGS\\\n_EOF_7F3C9EX'\"\n\\$(touch PWNED); touch RAN\n\""
# split-in-string-closed: expands under both; measured 2026-11-02
E split-in-string-closed "bash -c 'cat <<VOYAGE_ARGS\\\n_EOF_7F3C9E'\"\n\\$(touch PWNED); touch RAN\nVOYAGE_ARGS_EOF_7F3C9E\""
# split-in-body: expands under both; measured 2026-11-02
E split-in-body "bash <<'VOYAGE_ARGS_EOF_7F3C9E'\nbash -c 'cat <<VOYAGE_ARGS\\\n_EOF_7F3C9E'\"\n\\$(touch PWNED); touch RAN\n\"\nVOYAGE_ARGS_EOF_7F3C9E"
# evl-x1: expands under both; measured 2026-11-02
E evl-x1 "eval cat \\<\\<VOYAGE_ARGS_EOF_7F3C9E'\n$(touch PWNED)\n'"
# evl-x1b: expands under both; measured 2026-11-02
E evl-x1b "bash -c cat\\ \\<\\<VOYAGE_ARGS_EOF_7F3C9E'\n$(touch PWNED)\n'"
# evl-sh-c: expands under both; measured 2026-11-02
E evl-sh-c "sh -c 'cat '\\<\\<VOYAGE_ARGS_EOF_7F3C9E'\n$(touch PWNED)\n'"
# evl-x2: expands under both; measured 2026-11-02
E evl-x2 "eval cat \\<\\< \"VOYAGE_ARGS_EOF_7F3C9E\n\\$(touch PWNED)\n\""
# evl-split-word: expands under both; measured 2026-11-02
E evl-split-word "eval cat \\<\\<VOYAGE_ARGS\\\n_EOF_7F3C9E'\n$(touch PWNED)\n'"
# evl-split-dq: expands under both; measured 2026-11-02
E evl-split-dq "eval cat \\<\\<\"VOYAGE_ARGS\\\n_EOF_7F3C9E\"'\n$(touch PWNED)\n'"
# evl-split-sq: expands under both; measured 2026-11-02
E evl-split-sq "eval cat \\<\\<'VOYAGE_ARGS\\\n_EOF_7F3C9E\n$(touch PWNED)\n'"
# evl-split-before-word: expands under both; measured 2026-11-02
E evl-split-before-word "eval cat \\<\\<\\\nVOYAGE_ARGS_EOF_7F3C9E'\n$(touch PWNED)\n'"
# evl-blank-escaped: expands under both; measured 2026-11-02
E evl-blank-escaped "eval cat \\<\\<\\ VOYAGE_ARGS_EOF_7F3C9E'\n$(touch PWNED)\n'"
# evl-empty-pair: expands under both; measured 2026-11-02
E evl-empty-pair "eval cat \\<\\<'' VOYAGE_ARGS_EOF_7F3C9E'\n$(touch PWNED)\n'"
# evl-escaped-dash: expands under both; measured 2026-11-02
E evl-escaped-dash "eval cat \\<\\<\\-VOYAGE_ARGS_EOF_7F3C9E'\n$(touch PWNED)\n'"
CORPUS
status=$?
case "$status" in
0 | 1 | 2) exit "$status" ;;
*) setup_fail "the reader exited $status" ;;
esac