OWASP MCP Top 10 coverage¶
Spec: https://owasp.org/www-project-mcp-top-10/ (beta) Last verified (global): 2026-11-10
The evidence behind the README's MCP Top-10 table. Full: the controls that run on the @Airlock call path cover the risk where it reaches a tool call, and nothing the risk names is out of scope. Partial: part of the risk is out of scope, or the control that covers it runs only where the host calls it. No benchmark in this repository maps its corpus to MCP01-MCP10, so no row has a measured block-rate.
Risks¶
| Risk ID | Risk | Coverage | Why | Last verified | Measured block-rate |
|---|---|---|---|---|---|
| MCP01 | Token Mismanagement & Secret Exposure | Partial | On the call path, ArgumentSecretGuard refuses secrets in arguments when a policy sets it, and the sanitizer masks the secrets its detectors find in output; MCPProxyGuard runs only where a proxy calls it. Hard-coded credentials, token lifetime, and secrets the host keeps in model memory or its own logs are out of scope. |
2026-11-10 | not measured |
| MCP02 | Privilege Escalation via Scope Creep | Partial | Agent identity (step 2) and capability gating (step 4) run on every call. CredentialScope runs only where the host calls MCPProxyGuard.validate_tool_credentials(), and the union boundary only at CapabilityCapEngine.grant_lease(). The scopes an authorization server grants a server, and how they grow, are set outside the call boundary. Was Full until 2026-11-10. |
2026-11-10 | not measured |
| MCP03 | Tool Poisoning | Partial | Ghost-argument rejection, the tool-name deny list and SafePath / SafeURL check each call. The description-manifest guard runs only where the host checks a tool at admission, and compares declared arguments and side effects, not instructions written into a description. A poisoned description that steers the model into well-formed calls to allowed tools passes all four; the MCPTox replay counts how often. Was Full until 2026-11-10. |
2026-11-10 | not measured |
| MCP04 | Software Supply Chain Attacks & Dependency Tampering | Partial | The tool-name deny list refuses download_plugin_* / fetch_plugin_* calls on the call path. The Ox stdio defaults screen a server's launch command only where the host calls validate_stdio_command() before spawning it. A tampered package or dependency inside an MCP server is out of scope. Was Full until 2026-11-10. |
2026-11-10 | not measured |
| MCP05 | Command Injection & Execution | Full | Untrusted input reaches a command through a tool argument or a server's launch command, and a control covers each. On the call path, ToolArgSinkGuard (step 2.75) and ShellCommandPolicy (step 2.77) screen arguments when a policy sets them, the tool-name deny list (step 2) refuses shell-shaped tools, and sandbox=True runs the tool in E2B. The stdio guard screens launch commands and CodeSandboxBridgeGuard is a second argument screen; the host calls both. |
2026-11-10 | not measured |
| MCP06 | Intent Flow Subversion | Partial | ToolOutputTrustGuard checks each result after the tool runs when the config sets one, and @requires_human_oversight holds a decorated tool for an approver. Instructions the output screen does not flag, and prompts that reach the model outside a tool result, are out of scope. |
2026-11-10 | not measured |
| MCP07 | Insufficient Authentication & Authorization | Partial | Every listed control is a helper or preset the host calls at an OAuth or MCP protocol step (authorization, discovery, request routing, step-up, Tasks); the @Airlock wrapper calls none of them. Transport-level authentication stays with the MCP server. |
2026-11-10 | not measured |
| MCP08 | Lack of Audit and Telemetry | Partial | The @Airlock wrapper writes a JSON Lines record for every call it handles, refused calls included (on by default), and a hash-chained decision record when the config sets a DecisionLog. OpenTelemetry export and spans and metrics run only where the host wires them; the wrapper emits neither. Context changes and model interactions outside a tool call are not recorded. Was Full until 2026-11-10. |
2026-11-10 | not measured |
| MCP09 | Shadow MCP Servers | Partial | Attested admission and the LAN guard check a server the host routes through them, before its tools are used. Finding an MCP server deployed outside governance, which never reaches these checks, is out of scope. | 2026-11-10 | not measured |
| MCP10 | Context Injection & Over-Sharing | Partial | The sanitizer masks the PII and secrets its detectors find in each result (on by default), and ToolOutputTrustGuard envelopes flagged output when the config sets one. The workspace-scoped WorkspacePIIConfig applies only through sanitize_with_workspace_config(), which the host calls. Context the host shares or keeps across tasks, users or agents, and sensitive data no detector matches, are out of scope. Was Full until 2026-11-10. |
2026-11-10 | not measured |
Controls¶
Each control is named as the README names it. Runs: an @Airlock step (the # Step N comments in core.py), @Airlock execute, post-execution or wrapper (the decision record, written around the steps), a decorator of its own, or host-called: a helper the host calls itself, which the @Airlock wrapper never calls.
| Risk ID | Control | Runs | Module | Preset | Test |
|---|---|---|---|---|---|
| MCP01 | MCPProxyGuard rejects passthrough headers and enforces audience |
host-called: MCPProxyGuard.validate_request(), in an MCP proxy |
agent_airlock.mcp_proxy_guard |
— | tests/test_mcp_proxy_guard.py |
| MCP01 | ArgumentSecretGuard refuses secrets in arguments |
@Airlock step 2.76: when the policy sets argument_secret_guard |
agent_airlock.argument_secret_guard |
argument_secret_defaults |
tests/test_argument_secret_guard_0_10_29.py |
| MCP01 | sanitizer masks secrets in tool output | @Airlock post-execution: mask_secrets, on by default |
agent_airlock.sanitizer |
— | tests/test_sanitizer.py |
| MCP02 | SecurityPolicy(require_agent_id=True) |
@Airlock step 2: refuses a call with no agent identity |
agent_airlock.policy |
owasp_mcp_top_10_2026_policy |
tests/test_policy_identity.py |
| MCP02 | capability gating | @Airlock step 4: the policy's CapabilityPolicy against the tool's @requires |
agent_airlock.capabilities |
owasp_mcp_top_10_2026_policy |
tests/test_capabilities.py |
| MCP02 | CredentialScope |
host-called: MCPProxyGuard.validate_tool_credentials() |
agent_airlock.mcp_proxy_guard |
— | tests/test_credential_scope.py |
| MCP02 | runtime capability-union deny at grant time | host-called: CapabilityCapEngine.grant_lease() |
agent_airlock.capability_caps.engine |
— | tests/capability_caps/test_union.py |
| MCP03 | Ghost-arg rejection | @Airlock step 1: UnknownArgsMode.BLOCK |
agent_airlock.unknown_args |
— | tests/test_unknown_args.py |
| MCP03 | SafePath / SafeURL |
@Airlock execute: strict validation of arguments annotated with them |
agent_airlock.safe_types |
— | tests/test_safe_types.py |
| MCP03 | mcp_description_manifest_guard_defaults() |
host-called: the preset's check(description) when a tool is admitted |
agent_airlock.mcp_spec.description_manifest_guard |
mcp_description_manifest_guard_defaults |
tests/mcp_spec/test_description_manifest_guard.py |
| MCP03 | install_* deny |
@Airlock step 2: denied_tools |
agent_airlock.policy |
owasp_mcp_top_10_2026_policy |
tests/test_policy_presets.py |
| MCP04 | stdio_guard_ox_defaults() (Ox 2026-04-16 advisory) |
host-called: validate_stdio_command() before a server is spawned |
agent_airlock.mcp_spec.stdio_guard |
stdio_guard_ox_defaults |
tests/cves/test_ox_mcp_stdio.py |
| MCP04 | download_plugin_* / fetch_plugin_* deny |
@Airlock step 2: denied_tools |
agent_airlock.policy |
owasp_mcp_top_10_2026_policy |
tests/test_policy_presets.py |
| MCP05 | stdio_guard shell-metachar and deny-pattern rules |
host-called: validate_stdio_command() before a server is spawned |
agent_airlock.mcp_spec.stdio_guard |
stdio_guard_ox_defaults |
tests/cves/test_ox_mcp_stdio.py |
| MCP05 | ToolArgSinkGuard |
@Airlock step 2.75: when the policy sets tool_arg_sink_guard |
agent_airlock.tool_arg_sink_guard |
tool_arg_sink_defaults |
tests/test_tool_arg_sink_guard.py |
| MCP05 | ShellCommandPolicy on arguments |
@Airlock step 2.77: when the policy sets shell_command_policy |
agent_airlock.shell_command_policy |
shell_command_defaults |
tests/test_shell_command_policy_0_10_29.py |
| MCP05 | exec_* / run_* / system_* deny |
@Airlock step 2: denied_tools |
agent_airlock.policy |
owasp_mcp_top_10_2026_policy |
tests/test_policy_presets.py |
| MCP05 | CodeSandboxBridgeGuard for code-tool sandbox escapes |
host-called: the preset's guard.evaluate(args) |
agent_airlock.mcp_spec.code_sandbox_bridge_guard |
code_sandbox_bridge_defaults |
tests/mcp_spec/test_code_sandbox_bridge_guard.py |
| MCP05 | E2B sandbox (sandbox=True) |
@Airlock execute: @Airlock(sandbox=True) runs the tool in E2B, and refuses the call when E2B is missing or fails |
agent_airlock.sandbox |
— | tests/test_sandbox_correctness_0_10_17.py |
| MCP06 | ToolOutputTrustGuard envelopes injected-instruction output |
@Airlock post-execution: when AirlockConfig(output_trust_guard=...) sets one |
agent_airlock.tool_output_trust_guard |
untrusted_tool_output_defaults |
tests/test_output_trust_guard_0_10_29.py, tests/test_tool_output_trust_guard.py |
| MCP06 | @requires_human_oversight gates high-value actions |
own decorator: holds the decorated tool until an approver answers | agent_airlock.oversight |
— | tests/test_oversight.py |
| MCP07 | OAuth 2.1 + PKCE S256 helpers (a redirect URI with a fragment is refused, RFC 6749 section 3.1.2) | host-called: generate_pkce_challenge(), validate_redirect_uri() |
agent_airlock.mcp_spec.oauth |
— | tests/mcp_spec/test_oauth.py |
| MCP07 | iss mix-up validation |
host-called: validate_authorization_response_iss() |
agent_airlock.mcp_spec.oauth |
mcp_spec_2026_07_defaults |
tests/test_mcp_spec_2026_07_preset.py, tests/mcp_spec/test_oauth.py |
| MCP07 | issuer and resource in discovered metadata bound to the URLs the client asked for (RFC 8414 and RFC 9728 section 3.3) |
host-called: validate_metadata_issuer(), validate_protected_resource() |
agent_airlock.mcp_spec.oauth |
— | tests/mcp_spec/test_oauth.py |
| MCP07 | header/body routing integrity | host-called: the preset's check on each request | agent_airlock.mcp_spec.header_integrity |
mcp_spec_2026_07_header_integrity_defaults |
tests/test_mcp_header_integrity_preset.py |
| MCP07 | unsigned-_meta trust boundary |
host-called: the preset's check on each request's _meta |
agent_airlock.mcp_spec.meta_trust |
mcp_meta_trust_2026_07_defaults |
tests/test_mcp_meta_trust_preset.py |
| MCP07 | step-up scope guard | host-called: the preset's check at each step-up | agent_airlock.mcp_spec.step_up_scope_guard |
mcp_step_up_scope_2026_07_defaults |
tests/test_mcp_step_up_scope_preset.py |
| MCP07 | Tasks lifecycle and admission guards | host-called: the presets' checks on each Tasks request | agent_airlock.mcp_spec.tasks_lifecycle_guard, agent_airlock.mcp_spec.tasks_admission_guard |
mcp_tasks_lifecycle_2026_07_defaults, mcp_tasks_2026_07_28_defaults |
tests/test_mcp_tasks_lifecycle_preset.py, tests/test_mcp_tasks_2026_07_28_preset.py |
| MCP08 | JSON-Lines audit log | @Airlock post-execution: enable_audit_log, on by default; refused calls are recorded too |
agent_airlock.audit |
— | tests/test_audit.py |
| MCP08 | hash-chained decision log | @Airlock wrapper: a decision entry before the tool runs and an outcome entry after, when AirlockConfig(decision_log=...) sets one |
agent_airlock.conformance.decision_log |
— | tests/test_decision_record_0_10_29.py, tests/test_conformance_decision_log.py |
| MCP08 | OpenTelemetry export | host-called: OTelAuditExporter; the audit_otel_* config fields are never applied |
agent_airlock.audit_otel |
— | tests/test_audit_otel.py |
| MCP08 | spans and metrics | host-called: observability.configure() and observe; the @Airlock wrapper emits none |
agent_airlock.observability |
— | tests/test_new_features.py |
| MCP09 | Attested tool-server admission | host-called: admit_server_tool() or MCPProxyGuard.audit_tool_admission() |
agent_airlock.mcp_spec.attested_admission |
mcp_attested_admission_defaults |
tests/test_attested_admission.py |
| MCP09 | LAN unauthenticated-MCP guard | host-called: LANUnauthRCEGuard on a server's registration |
agent_airlock.mcp_spec.lan_unauth_rce_guard |
lan_unauth_mcp_guard |
tests/mcp_spec/test_lan_unauth_rce_guard.py |
| MCP10 | PII and secret sanitizer | @Airlock post-execution: mask_pii and mask_secrets, on by default |
agent_airlock.sanitizer |
— | tests/test_sanitizer.py |
| MCP10 | workspace-scoped config | host-called: sanitize_with_workspace_config() with a WorkspacePIIConfig; the @Airlock sanitizer does not read it |
agent_airlock.sanitizer |
— | tests/test_workspace_pii.py |
| MCP10 | ToolOutputTrustGuard untrusted-data envelope |
@Airlock post-execution: when AirlockConfig(output_trust_guard=...) sets one |
agent_airlock.tool_output_trust_guard |
untrusted_tool_output_defaults |
tests/test_output_trust_guard_0_10_29.py |