GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — mcpproxy-go/scripts/tpa-lab-redteam-round2.sh at main · DocSpring/mcpproxy-go

Expand file tree

/

tpa-lab-redteam-round2.sh

Copy path
More file actions
More file actions

Latest commit

 

History

History
History
executable file
·
256 lines (233 loc) · 12.8 KB
/

tpa-lab-redteam-round2.sh

Copy path

File metadata and controls

executable file
·
256 lines (233 loc) · 12.8 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#!/usr/bin/env bash
# Round-2 red-team experiments (2026-10-20) against the hardened build
# (feat/tpa-attack-lab merged with latest origin/main; RT-01..RT-07 verified
# fixed). New attack surfaces:
#
# R2-1 auto-baseline poisoning: an UNQUARANTINED server's first toolset is
# auto-approved with no scan — even blatantly poisoned descriptions.
# R2-2 unquarantine race (control): rug pull while quarantined, then call
# the changed tool immediately after unquarantine, before the sweep.
#
# Same safety model as the round-1 suite: malicious upstreams run in Docker
# (distroless, network none, read-only, caps dropped).
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
TEMPLATE="${TPA_LAB_TEMPLATE:-$PROJECT_DIR/tests/tpa-lab-config-docker.json}"
BINARY="${TPA_LAB_BINARY:-$PROJECT_DIR/mcpproxy}"
API_KEY="tpa-lab-key"
LISTEN="127.0.0.1:8081"
API_URL="http://$LISTEN/api/v1"
MCP_URL="http://$LISTEN/mcp"
IMAGE="${IMAGE:-tpa-attack-lab:lab}"
RUN_ROOT="${TPA_LAB_RUN_ROOT:-$PROJECT_DIR/test-data-tpa-lab-docker}"
ONLY="${ONLY:-}"
DAEMON_PID=""
# shellcheck source=lib/tpa-lab-verdicts.sh
source "$SCRIPT_DIR/lib/tpa-lab-verdicts.sh"
# Same guards as the round-1 runner (Sol 6.1 round-2 review, findings 4-7):
# inherited MCPPROXY_* overrides (MCPPROXY_DATA, MCPPROXY_LISTEN, …) would beat
# the generated config's data_dir and listen address for the daemon AND the
# CLI calls — drop them before any malicious fixture runs; start_daemon also
# pins --data-dir and --listen.
lab_scrub_mcpproxy_env
# One fresh run dir per invocation: approvals left by an earlier run would
# otherwise make R2-1 a NON-first-seen pass (pending/approved records already
# exist and never enter the baseline branch), so a broken gate could still
# score HOLDS. R2-2's two stages share one data dir INSIDE this invocation.
mkdir -p "$RUN_ROOT"
RUN_DIR="${RUN_DIR:-$(mktemp -d "$RUN_ROOT/rXXXX")}"
mkdir -p "$RUN_DIR"
ln -sfn "$RUN_DIR" "$RUN_ROOT/round2-latest"
log() { echo "[r2] $*"; }
if lab_port_busy 8081; then
echo "ERROR: port 8081 is already in use; refusing to run (nothing was stopped)." >&2
echo "Inspect the listener with 'lsof -nP -iTCP:8081 -sTCP:LISTEN' and stop it yourself if it is a stale lab daemon." >&2
exit 1
fi
if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
docker build -q -f "$PROJECT_DIR/tests/tpa-attack-lab/Dockerfile" -t "$IMAGE" "$PROJECT_DIR" >/dev/null || exit 1
(cd "$PROJECT_DIR" && go build -o ./mcpproxy ./cmd/mcpproxy) || exit 1
fi
# make_cfg <name> <profile> <stage> <data_dir> [quarantined:yes|no]
make_cfg() {
local cfg="$RUN_DIR/$1.json"
python3 - "$TEMPLATE" "$cfg" "$2" "$3" "$4" "$5" <<'PY'
import json, sys
tpl, out, profile, stage, data_dir, quar = sys.argv[1:7]
cfg = json.load(open(tpl)); cfg["data_dir"] = data_dir
for s in cfg["mcpServers"]:
if s["name"] == "tpa-lab-attacker":
s["env"] = {"TPA_LAB_PROFILE": profile, "TPA_LAB_STAGE": stage}
s["quarantined"] = (quar == "yes")
json.dump(cfg, open(out, "w"), indent=2)
PY
[[ $? -eq 0 ]] || { echo "lab config generation failed for $cfg" >&2; return 1; }
# Run the image this invocation built (IMAGE override), never the template's
# default tag; refuses a config whose upstreams are not Docker-isolated.
lab_config_use_image "$cfg" "$IMAGE" >&2 || { echo "refusing lab config $cfg" >&2; return 1; }
echo "$cfg"
}
connected() {
curl -sf -H "X-API-Key: $API_KEY" "$API_URL/servers" 2>/dev/null | python3 -c '
import json,sys
try:
d=json.load(sys.stdin); s=d.get("data",{}).get("servers",[])
print(sum(1 for x in s if x.get("status") in ("connected","Ready","ready")))
except Exception: print(0)' || echo 0
}
start_daemon() {
local cfg="$1" logf="$2"
lab_config_require "$cfg" || { echo "refusing to start the daemon without a checked lab config"; return 1; }
local ddir
ddir="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["data_dir"])' "$cfg")" || return 1
export MCPPROXY_TRAY_ENDPOINT="unix://$ddir/mcpproxy.sock"
"$BINARY" serve --config "$cfg" --data-dir "$ddir" --listen "$LISTEN" --log-level=info >"$logf" 2>&1 &
DAEMON_PID=$!
for i in $(seq 1 60); do
curl -sf -o /dev/null -H "X-API-Key: $API_KEY" "$API_URL/servers" && break
kill -0 "$DAEMON_PID" 2>/dev/null || { echo "daemon exited"; tail -10 "$logf"; return 1; }
sleep 1
done
local a b
for i in $(seq 1 60); do a=$(connected); sleep 2; b=$(connected); [[ "$a" -ge 2 && "$b" -ge 2 ]] && return 0; done
echo "WARNING: upstreams not stable"; return 0
}
stop_daemon() {
[[ -n "$DAEMON_PID" ]] && kill "$DAEMON_PID" 2>/dev/null
[[ -n "$DAEMON_PID" ]] && wait "$DAEMON_PID" 2>/dev/null
DAEMON_PID=""; sleep 1
}
trap stop_daemon EXIT
tool_status() {
LC_ALL=C "$BINARY" upstream inspect tpa-lab-attacker --config "$CURRENT_CFG" 2>/dev/null \
| awk -v t="$1" '$1==t {print $2; found=1; exit} END {if (!found) print "not-found"}'
}
wait_status() {
local s
for i in $(seq 1 "${3:-30}"); do s="$(tool_status "$1")"; [[ "$s" != "not-found" ]] && { echo "$s"; return; }; sleep 1; done
echo "not-found"
}
wait_status_not() {
local tool="$1" avoid="$2" timeout="${3:-90}" s
for i in $(seq 1 "$timeout"); do
s="$(tool_status "$tool")"
[[ "$s" != "not-found" && "$s" != "$avoid" ]] && { echo "$s"; return; }
sleep 1
done
echo "$s"
}
# unquarantine fails on a non-2xx answer, so an experiment cannot report HOLDS
# while the server stayed quarantined (server-level quarantine would then be
# what blocked the call, not the protection under test).
unquarantine() { curl -sf -X POST -H "X-API-Key: $API_KEY" "$API_URL/servers/tpa-lab-attacker/unquarantine" >/dev/null; }
# mcp_call <tool-name> <args-json> — one-shot MCP session over streamable HTTP.
# args_json is passed as a JSON-ENCODED STRING (call_tool_read's contract).
mcp_call() {
local tool="$1" args="$2" hdr payload
hdr=$(mktemp); payload=$(mktemp)
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"r2","version":"1"},"capabilities":{}}}' >"$payload"
curl -s -X POST "$MCP_URL" -H 'Content-Type: application/json' -D "$hdr" -d @"$payload" >/dev/null
local sid; sid=$(grep -i "Mcp-Session-Id" "$hdr" | tr -d '\r\n' | sed 's/[^:]*: *//')
printf '{"jsonrpc":"2.0","method":"notifications/initialized"}' >"$payload"
[[ -n "$sid" ]] && curl -s -X POST "$MCP_URL" -H 'Content-Type: application/json' -H "Mcp-Session-Id: $sid" -d @"$payload" >/dev/null
local args_json_string; args_json_string=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$args")
printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"call_tool_read","arguments":{"name":"tpa-lab-attacker:%s","args_json":%s}}}' "$tool" "$args_json_string" >"$payload"
[[ -n "$sid" ]] && curl -s -X POST "$MCP_URL" -H 'Content-Type: application/json' -H "Mcp-Session-Id: $sid" -d @"$payload"
rm -f "$hdr" "$payload"
}
RESULTS=()
record() { RESULTS+=("$1"); log "$1"; echo "----------------------------------------"; }
# ================================================================== R2-1
if [[ -z "$ONLY" || "$ONLY" == "R2-1" ]]; then
log "R2-1: auto-baseline poisoning — UNQUARANTINED server, full 18-vector catalog, NO scan"
CURRENT_CFG="$(make_cfg r2-1 all 1 "$RUN_DIR/r2-1-data" no)" || { record "R2-1 INCONCLUSIVE: lab config refused"; exit 1; }
start_daemon "$CURRENT_CFG" "$RUN_DIR/r2-1.log" || { record "R2-1: INCONCLUSIVE (daemon)"; exit 1; }
sleep 5
sum_st="$(tool_status summarize_text)" # poisoned override vector
fin_st="$(tool_status finance_convert_currency)" # poisoned exfil vector
if [[ -z "$sum_st" ]]; then
log "inspect output (for debugging the status parse):"
"$BINARY" upstream inspect tpa-lab-attacker --config "$CURRENT_CFG" 2>&1 | head -12 | sed 's/^/ /'
sum_st="$(tool_status summarize_text)"
fi
log "statuses with no operator action: summarize_text=$sum_st finance_convert_currency=$fin_st"
approved_count=$("$BINARY" upstream inspect tpa-lab-attacker --config "$CURRENT_CFG" 2>/dev/null | grep -c "approved" || true)
log "approved rows in inspect output: $approved_count"
# Is the poisoned tool actually CALLABLE through the proxy?
call_out="$(mcp_call summarize_text '{"text":"hello"}')"
if echo "$call_out" | grep -q 'would_have_executed'; then
call_result="CALLED — poisoned tool answered through the proxy"
elif echo "$call_out" | grep -qiE "blocked|quarantin|pending|security"; then
call_result="BLOCKED"
else
call_result="UNCLEAR: $(echo "$call_out" | head -c 160)"
fi
log "call summarize_text → $call_result"
# Would a scan have flagged it? (operator-triggered, i.e. after the fact)
"$BINARY" security scan tpa-lab-attacker --config "$CURRENT_CFG" >"$RUN_DIR/r2-1-scan.txt" 2>&1
verdict="$(grep -oiE 'dangerous|warnings|clean' "$RUN_DIR/r2-1-scan.txt" | head -1)"
log "post-hoc operator scan verdict: ${verdict:-unknown}"
held_re='^(pending|changed|blocked)'
if [[ "$sum_st" == "approved" && "$call_result" == CALLED* ]]; then
record "R2-1 HOLE: unquarantined first-seen server auto-baselined the POISONED catalog (summarize_text=$sum_st, ${approved_count} approved rows) and the poisoned tool is callable; the operator-triggered scan says '$verdict' only AFTER the tools are live"
elif [[ "$sum_st" == "approved" || "$fin_st" == "approved" ]]; then
record "R2-1 HOLE: a poisoned first-seen tool was auto-baselined (summarize_text=$sum_st finance_convert_currency=$fin_st); call: $call_result; scan='$verdict'"
elif [[ "$sum_st" =~ $held_re && "$fin_st" =~ $held_re && "$call_result" == "BLOCKED" ]]; then
# Acceptance (round-2 report): BOTH poisoned vectors pending or worse AND the
# call blocked. An empty/unparsed status or an unclear call never scores HOLDS.
record "R2-1 HOLDS: poisoned tools not auto-approved (summarize_text=$sum_st finance_convert_currency=$fin_st, ${approved_count} approved rows for the benign controls) and the call is blocked"
else
record "R2-1 INCONCLUSIVE: summarize_text='$sum_st' finance_convert_currency='$fin_st' call='$call_result' scan='$verdict'"
fi
stop_daemon
fi
# ================================================================== R2-2
if [[ -z "$ONLY" || "$ONLY" == "R2-2" ]]; then
log "R2-2 (control): rug pull while quarantined → immediate call after unquarantine (race window)"
CURRENT_CFG="$(make_cfg r2-2 facade 1 "$RUN_DIR/r2-2-data" yes)" || { record "R2-2 INCONCLUSIVE: lab config refused"; exit 1; }
start_daemon "$CURRENT_CFG" "$RUN_DIR/r2-2.log" || { record "R2-2: INCONCLUSIVE (daemon)"; exit 1; }
unquarantine || { record "R2-2 INCONCLUSIVE: stage-1 unquarantine refused"; stop_daemon; exit 1; }
s="$(wait_status calendar_get_events)"
curl -sf -X POST -H "X-API-Key: $API_KEY" -H 'Content-Type: application/json' -d '{"approve_all":true}' \
"$API_URL/servers/tpa-lab-attacker/tools/approve" >/dev/null \
|| { record "R2-2 INCONCLUSIVE: stage-1 facade approval refused"; stop_daemon; exit 1; }
log "stage-1 facade approved (calendar=$s)"
stop_daemon
CURRENT_CFG="$(make_cfg r2-2 facade 2 "$RUN_DIR/r2-2-data" yes)" || { record "R2-2 INCONCLUSIVE: lab config refused"; exit 1; }
start_daemon "$CURRENT_CFG" "$RUN_DIR/r2-2-2.log" || { record "R2-2: INCONCLUSIVE (restart)"; exit 1; }
sleep 6
during="$(tool_status calendar_get_events)"
log "while still quarantined, armed contract state: calendar=$during (RT-05 verification)"
unquarantine || { record "R2-2 INCONCLUSIVE: stage-2 unquarantine refused"; stop_daemon; exit 1; }
# Call IMMEDIATELY — before the re-discovery sweep could have run.
call_out="$(mcp_call calendar_get_events '{"date":"2026-10-20"}')"
if echo "$call_out" | grep -qiE "blocked|changed|security|pending|not approved"; then
call_result="BLOCKED"
elif echo "$call_out" | grep -q '"vector"'; then
call_result="CALLED"
else
call_result="UNCLEAR: $(echo "$call_out" | head -c 160)"
fi
after="$(wait_status_not calendar_get_events approved 60)"
log "immediate call after unquarantine → $call_result; settled status: calendar=$after"
if [[ "$call_result" == "BLOCKED" ]]; then
record "R2-2 HOLDS: changed tool blocked even in the immediate post-unquarantine window (during-quarantine state: $during, settled: $after)"
elif [[ "$call_result" == "CALLED" ]]; then
record "R2-2 HOLE: armed tool CALLABLE immediately after unquarantine before the sweep (during=$during, settled=$after)"
else
record "R2-2 INCONCLUSIVE: $call_result (during=$during, settled=$after)"
fi
stop_daemon
fi
echo
log "===== round-2 summary ====="
printf '%s\n' "${RESULTS[@]-}"
# Fail closed like the round-1 suite: any HOLE, PARTIAL or INCONCLUSIVE line
# (or no experiment selected) is a non-zero exit.
if suite_exit_status "${RESULTS[@]-}"; then
log "round-2 result: PASS (every finding holds)"
exit 0
fi
log "round-2 result: FAIL (a hole was confirmed or an experiment was inconclusive)"
exit 1
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.