#!/usr/bin/env bash # Regression test for the gate-forgery attack confirmed by the pre-push # security review. # # Threat: the build gates execute code authored in the PR under review. That # code runs as the same user and can write the gate table, either overriding a # genuine FAIL or pre-seeding a PASS for a gate that has not run yet. Either # way it forges the exact signal the runner exists to produce. # # Defense under test: the runner records each key exactly once, so any duplicate # key means a second writer touched the table, and every decision path calls # assert_gate_table_intact first and fails closed. set -euo pipefail TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$TESTS_DIR/.." && pwd)" pass=0 fail=0 check() { # check local name="$1" expect="$2" rc="$3" got="ok" [ "$rc" -eq 0 ] || got="err" if [ "$got" = "$expect" ]; then pass=$((pass + 1)) else echo "FAIL: $name (expected $expect, got $got, rc=$rc)" fail=$((fail + 1)) fi } tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT mkdir -p "$tmp/artifacts" "$tmp/state" export WORKSPACE_DIR="$tmp" export ARTIFACTS_DIR="$tmp/artifacts" export STATE_DIR="$tmp/state" export GATE_STATUS_FILE="$tmp/state/gate-status.tsv" # shellcheck source=../scripts/lib.sh source "$REPO_DIR/scripts/lib.sh" # 1. An untampered table passes. record_gate frontend.install PASS "log" record_gate frontend.lint FAIL "exit 1" rc=0; ( assert_gate_table_intact ) >/dev/null 2>&1 || rc=$? check "clean table passes integrity check" ok "$rc" # 2. Pre-seeding: PR code writes a forged PASS for a gate that has not run yet, # then the runner records the genuine FAIL. First-wins would return the # forged PASS, so the duplicate must be detected. printf 'frontend.build\tPASS\tforged by PR code\n' >>"$GATE_STATUS_FILE" record_gate frontend.build FAIL "exit 2" rc=0; ( assert_gate_table_intact ) >/dev/null 2>&1 || rc=$? check "pre-seeded forged PASS is detected" err "$rc" # 3. The same tampered table must block a review decision. cp "$TESTS_DIR/fixtures/artifacts/frontend-pr.json" "$ARTIFACTS_DIR/" rc=0 env -i PATH="$PATH" HOME="$HOME" \ WORKSPACE_DIR="$tmp" ARTIFACTS_DIR="$tmp/artifacts" STATE_DIR="$tmp/state" \ REVIEW_TYPE=frontend \ "$REPO_DIR/scripts/validate-review-output.sh" \ "$TESTS_DIR/fixtures/reviews/valid-approve.md" >/dev/null 2>&1 || rc=$? check "tampered table blocks review validation" err "$rc" # 4. run_gate strips the Actions runner-command channels from the child # environment, so PR code cannot poison later steps via $GITHUB_ENV. probe="$tmp/probe.sh" cat >"$probe" <<'PROBE' #!/usr/bin/env bash [ -z "${GITHUB_ENV:-}" ] || { echo "GITHUB_ENV leaked"; exit 1; } [ -z "${GITHUB_PATH:-}" ] || { echo "GITHUB_PATH leaked"; exit 1; } [ -z "${RUNNER_TEMP:-}" ] || { echo "RUNNER_TEMP leaked"; exit 1; } [ -z "${GATE_STATUS_FILE:-}" ] || { echo "GATE_STATUS_FILE leaked"; exit 1; } exit 0 PROBE chmod +x "$probe" rc=0 GITHUB_ENV="$tmp/ghenv" GITHUB_PATH="$tmp/ghpath" RUNNER_TEMP="$tmp" \ run_gate probe.env probe.log "$probe" >/dev/null 2>&1 || rc=$? check "run_gate strips runner-command channels from PR code" ok "$rc" echo "gate-integrity: $pass passed, $fail failed" [ "$fail" -eq 0 ]