OWASP MCP Top 10 — coverage

How MCP Hangar maps against the OWASP MCP Top 10 (2025).

Read it beside what a verdict establishes, which says what each individual record from these mechanisms does and does not prove.

This page is deliberately honest about scope. Hangar is a policy enforcement plane: it enforces deterministic policy on the MCP call path and produces an attributable audit trail. It does not parse prompt or tool-argument semantics and will not pretend to — so several categories are out of scope by design, and a few are partially covered with the remaining enforcement tracked in open issues. Each row says which.

#CategoryHangar postureMechanism / tracking
MCP01Token Mismanagement & Secret ExposurePartialRFC 8707 audience binding, RFC 9728 protected-resource metadata, JWT/OIDC + JWKS, API keys (SHA-256) with optional expiry; DNS-exfiltration channel closed (operator#56). Open: mcp-hangar add writes a prompted secret in plaintext (mcp-hangar#1653)
MCP02Privilege Escalation via Scope CreepPartialPer-tenant scoping, tool allow/deny lists, human approval gates on privileged calls. Open: a global developer can withdraw a tool for all tenants (mcp-hangar#1656)
MCP03Tool PoisoningPartialTool-schema digest pinning, per tenant or for all tenants (detects rug-pull / description drift), enforcement block (default), warn or audit. Since 2.25.0 pinned servers are re-listed every tool_projection.pin_recheck_interval_s (60 s by default), so unannounced upstream drift is refused within about one interval; before, it was served until a restart or tools/list_changed (mcp-hangar#1693)
MCP04Software Supply Chain & Dependency TamperingPartialHangar's own artifacts are cosign-signed + SBOM'd, chart digests pinned (ADR-004); MCP-server image digest pinning is checked at admission (operator v0.13.0) but the policy defaults to warn -- block is opt-in at startup and a per-server annotation waives it
MCP05Command Injection & ExecutionOut of scope (by design)Hangar does not inspect tool-call arguments for injection. Container-mode command allow-list constrains the server process, not call semantics
MCP06Intent Flow SubversionOut of scope (by design)Intent/prompt semantics are not parsed. Approval gates offer a human checkpoint, not intent detection
MCP07Insufficient Authentication & AuthorizationPartialRFC 8707 / RFC 9728, JWT/OIDC + JWKS, per-tenant scoping, approval gates, end-to-end identity propagation. Open: oidc.clock_skew_leeway_seconds is not parsed (mcp-hangar#1654)
MCP08Lack of Audit and TelemetryCoveredIdentity-attributed audit trail per tool call, including calls a gate refuses; SIEM export (CEF, LEEF 2.0, RFC 5424 syslog, JSON-lines); Prometheus metrics; K8s events
MCP09Shadow MCP ServersPartialRegistry of MCPServer resources, admission validation of their spec, per-server egress NetworkPolicy, plus a pod-registration webhook (operator v0.13.0) in namespaces labelled mcp-hangar.io/enforce-egress=true. It keys on the pod's own mcp-hangar.io/provider label, so it catches misconfiguration, not concealment; since v0.17.5 that label cannot be added or changed after admission. Since core 2.32.0 a server pinned by its key refuses a TLS peer without a listed key, so such a peer cannot answer in its place
MCP10Context Injection & Over-SharingOut of scope (by design)Prompt/context content is not inspected. Per-tenant scoping + audit limit blast radius but do not detect context injection

Covered

MCP08 — Audit & Telemetry. Every tool call -- completed, failed, or refused by a gate -- carries the caller's identity into the audit record, exported to SIEM in CEF, LEEF 2.0, RFC 5424 syslog, and JSON-lines (OTLP is a separate trace/audit-span path, not a SIEM format). Prometheus metrics and Kubernetes events cover the operational side. Since 2.25.0 a SIEM export the gateway cannot perform -- an unrecognised MCP_COMPLIANCE_FORMAT, or an output file it cannot append to -- refuses startup, and a write that fails later is counted in mcp_hangar_compliance_export_failures_total and reported as degraded on the health surfaces while calls are still served; alert on the counter. Before 2.25.0 either case disabled or dropped the export with only a log line (mcp-hangar#1701) -- see Compliance Export.

Partial (enforcement tracked)

MCP01 — Token/Secret Exposure. Audience binding and PRM address token reuse; API-key hygiene (SHA-256, optional expiry) and closing the DNS-exfiltration channel (operator#56, fixed) reduce secret leakage. Credential brokering for upstreams (so servers never see raw upstream tokens) is noted as future work in the MCPEgressPolicy epic (operator#53). Open: mcp-hangar add writes the secret it prompts for in plaintext to a mode-0644 config.yaml (mcp-hangar#1653).

MCP02 — Scope Creep. Tool allow/deny lists and per-tenant scoping bound what a caller can reach; approval gates catch privileged calls. There is no automatic detection of gradual scope expansion — that would require behavioral baselining, which the shipped core deliberately does not do. One open defect weakens the bound: a global developer can withdraw or restore a tool for every tenant (mcp-hangar#1656). Fixed in 2.25.0, and still present on an older gateway: a tools: policy with one invalid field was discarded whole, with only a warning (mcp-hangar#1648), and a policy list written as a string was read as its characters (mcp-hangar#1718); both now refuse the configuration.

MCP03 — Tool Poisoning. A poisoned tool changes its schema/description after approval (a "rug pull"). Hangar pins tool schemas -- per tenant, or for every caller under mcp_servers.<name>.tool_projection.pins -- and, in the default block mode, refuses calls whose schema digest no longer matches the pinned value (ToolDigestMismatchError); warn and audit record the drift instead. This is the direct answer to MCP03. The comparison is made against the catalogue Hangar last fetched, and since 2.25.0 Hangar re-lists every READY pinned server every tool_projection.pin_recheck_interval_s (60 s by default, 0 turns it off), so an upstream that changes its schema without announcing tools/list_changed is refused from the next call after the pass that sees it -- a window of one interval plus the listing time. Before 2.25.0, or with the interval set to 0, such a change is served until Hangar restarts or re-lists the server (mcp-hangar#1693).

MCP07 — Insufficient Auth. Authentication and authorization are Hangar's core. Tokens are audience-bound (RFC 8707) so a token minted for one resource cannot be replayed against another; every trusted issuer is advertised through RFC 9728 protected-resource metadata. JWT/OIDC with JWKS validation and per-tenant scoping gate who may call what, and approval gates add a human decision point for high-risk calls. Caller identity is propagated end-to-end. Open: oidc.clock_skew_leeway_seconds is never parsed (mcp-hangar#1654). Fixed in 2.25.0: the auth REST routes took the recorded actor (assigned_by, created_by, ...) from the request body (mcp-hangar#1649); they now record the authenticated principal and refuse those fields with 422, so an auth event from an older gateway names whoever its body said.

MCP04 — Supply Chain. Hangar signs and SBOMs its own release artifacts and pins chart digests. Since operator v0.13.0 an admission check evaluates whether an MCPServer's container image is digest-pinned — but the policy ships as warn, which admits the pod and records a warning. Enforcement means setting the operator's image-digest policy to block and not waiving it with the per-server annotation. Until an operator is configured that way, mutable tags are accepted in practice.

MCP09 — Shadow MCP Servers. OWASP MCP09's prevention control #1 is a registry tied to deployment where unregistered instances fail. Hangar provides the registry (MCPServer CRDs), admission validation of their spec, per-server egress policy, and — since operator v0.13.0 — a pod-registration webhook and default-deny egress in namespaces labelled mcp-hangar.io/enforce-egress=true.

The webhook rejects a pod carrying mcp-hangar.io/provider=<name> when no MCPServer of that name exists in the namespace; a pod without that label is admitted as "not an MCP-server pod". Before operator v0.17.5 the check ran on pod creation only, so an admitted unlabelled pod could be labelled into a registered server afterwards and inherit that server's egress. Since v0.17.5 (with chart 0.12.18, which lists UPDATE on the rule) the webhook also gates updates: adding or changing the label on an admitted pod is denied, removing it is allowed. It therefore catches a provider pointed at a server nobody registered — a misconfiguration — rather than a workload that never announces itself, which is what shadow means. What constrains that one is the default-deny egress at L3/L4, not admission.

Since core 2.32.0 (operator 0.17.32 for Kubernetes), a server can be pinned by its key (ADR-030). Hangar accepts the connection only when the TLS peer presents a key on the server's list, so a pod that inherits a registered server's address, or answers in its place without a listed key, is refused in the handshake. This covers the servers that declare a key list and nothing else. It does not find a server nobody registered, and a holder of a listed key, a stolen one included, is accepted until the key is removed.

So read MCP09 coverage as "registry, scoped egress, a gate against mislabelled providers in opted-in namespaces, and a key check for key-pinned servers", not "shadow MCP prevented".

Out of scope by design

MCP05, MCP06, MCP10 are prompt/intent/context-semantic risks: command injection through tool arguments, intent-flow subversion, and context injection / over-sharing. Detecting these means interpreting the meaning of prompts, arguments, and responses. Hangar is a deterministic policy plane — it enforces who-may-call-what and records what happened; it does not guess intent, and adding a probabilistic classifier would contradict its design (binary policy outcomes, no false-positive triage). These categories belong to a different layer (prompt-firewall / semantic-analysis tools) and Hangar does not claim to cover them.


Last reviewed 2026-10-04 against core 2.24.0. Postures marked "Partial" link the open issues that would move them to "Covered".