Repository navigation
0.8.0 — Reference-SDK rigs for A2A and MCP
Both protocol clients were validated against hand-written mocks, which only ever confirm that the scanner agrees with itself. Pointing each one at its reference implementation turned that agreement into test failures: five defects in A2A, six in MCP. This release exists because 0.7.0 reports clean scans it has not earned — a strict A2A v1.0 server answered -32601 to every call while the active scan reported success, and a fully populated MCP server over Streamable HTTP fingerprinted as zero tools with every downstream audit running over nothing. A scanner that says "nothing found" because it never arrived is worse than no scanner.
Reference-SDK rigs
- A2A rig (
lab/a2a/agent.py,pip install -e '.[lab]') built on the referencea2a-sdk. It publishes a deliberately weak v1.0 AgentCard and echoes task text back as an artifact, so the passive audit and the injection canary both have something real to find. Serves strict v1.0 by default and the legacy v0.3.x vocabulary underA2A_LAB_COMPAT=1. - MCP rig (
lab/mcp/server.py, plus a compose service) built on the referencemcpSDK. Both transports and both protocol generations answer on one endpoint. Deliberately vulnerable along each detector axis: a poisoned tool description, an unguardedread_file, afetch_urlthat dereferences anything includingfile://, a resource carrying both an injection directive and a Markdown-image beacon, and a templated resource whose description smuggles a directive of its own. It exposed four transport and reporting defects within the first hour.
A2A wire correctness
- The client spoke the v0.3.x JSON-RPC vocabulary unconditionally and sent no
A2A-Versionheader. The dialect is now resolved from the discovered card:supportedInterfacesmeans v1.0, a top-levelurlwithpreferredTransportmeans v0.3.x, and an explicitprotocolVersionoverrides the shape. - A fully secured v0.3.x card was reported HIGH as enforcing no authentication. Both generations name the mandatory subset under a different key —
securityRequirementsin v1.0,securityin v0.3.x — and only the v1.0 spelling was read. A false positive at HIGH, aimed precisely at the operators who got it right. - An agent replying with a
Messageinstead of aTaskwent unscanned.SendMessageResponseis a oneof; the client looked only for a Task, then polled a task id it had never been given. Task history is deliberately excluded from the scan, since it contains the probe payload and would make every agent self-match. - Dropped the invented
params.id. Neither generation lets a client choose the id of a task it is creating. Seven unit mocks asserted that field, which is precisely why the divergence survived. probe_unauthorized_cancelaccepted any JSON-RPC error as proof the server had rejected the call, so-32601was reported as "server behaved safely". Only the task-domain codes count as a rejection now; everything else is inconclusive.- Removed two card checks reading fields that exist in neither generation, so no configuration could ever clear them.
MCP wire correctness
- Streamable HTTP scanned every server clean. The transport dropped both pieces of per-connection state: a server that mints an
Mcp-Session-Idoninitializerejects every later request without it, and the enumeration helpers turned that error into an empty list. Remote and hosted servers are exactly the ones an operator points a scanner at, so the blind spot covered the whole practical MCP surface. The negotiated revision failed more quietly — with noMCP-Protocol-Versionheader the spec tells a server to assume 2025-03-26, so the scanner reasoned about a generation the server was not speaking. - A refused listing read as an empty inventory. All three list methods returned
[]on error, the same value they return for a server that genuinely has none, so the coverage claim and the clean result were indistinguishable. Failures now surface as anenumeration_gapcheck, with the JSON-RPC code deciding severity:-32601means the surface does not exist (INFO); anything else means it may exist and went unexamined (MEDIUM). - Every MCP finding reached HTML, Markdown, SARIF and JUnit as module
unknownwith a blank title and no taxonomy — a CRITICAL tool-poisoning hit filed under SARIF ruleIdMAS-SENTRY-UNKNOWN.from_mcp_checkwas written, unit-tested and never called. Full coverage of the function was exactly what hid the missing call, so the new tests drive the CLI instead. StdioConfig.timeoutwas declared and never applied. A server that accepted a request and answered nothing stopped the scan for good — and whether it answers is the target's choice. A pentest tool the scanned host can hang is a denial of service on its own operator. Reads now poll with a deadline; a timeout or a closed pipe becomes a JSON-RPC error rather than a hang, and a closed pipe reports the exit code and stderr tail.- Paginated listings are walked to the end. All three list methods read page one and stopped. A server that repeats a cursor, or never stops issuing them, would spin the scanner in place, so both end the walk and are recorded as gaps.
- Templated resources are enumerated and scanned (
resources/templates/list, checkresource_template). They never appear inresources/list. The body needs a parameter value and stays out of reach, but the name and description do not — and those are what an agent weighs before expanding the template and pulling the result into context. - MCP resource-content auditing (
--checks resources) and structuredCallToolResultreading: content-block extraction across text, image, audio, embedded-resource and resource-link types, with base64 decoding. The SSRF and traversal probes previously matched indicators against a truncated Python repr, so content delivered as base64 could not match at all.
Upgrading
No API changes. Anyone scanning MCP over HTTP or A2A v1.0 should re-run against targets previously reported clean — those results were false negatives, not findings.
Full Changelog: v0.7.0...v0.8.0
What's Changed
New Contributors
Full Changelog: v0.7.0...v0.8.0