{
  "matrixId": "mcp-security-controls",
  "version": "2026-08-26",
  "targetProtocolRevisions": [
    "2026-07-28"
  ],
  "scope": "Release controls for remote HTTP, proxy, host, and local MCP deployments.",
  "controls": [
    {
      "id": "MCP-AUTH-01",
      "title": "Bind access tokens to the MCP resource",
      "appliesTo": [
        "client",
        "server"
      ],
      "risk": "A valid token issued for another service is accepted by the MCP server.",
      "control": "Send bearer tokens only in the Authorization header. Validate signature, issuer, expiry, audience, and operation scope on every HTTP request, then reject tokens not issued for this MCP resource.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "specification",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#access-token-usage",
          "requirementLevel": "MUST",
          "summary": "HTTP requests carry authorization in the header, and servers validate tokens as OAuth resource servers."
        },
        {
          "publisher": "MCP",
          "sourceType": "specification",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-handling",
          "requirementLevel": "MUST",
          "summary": "The server validates that a token was issued specifically for it as the intended audience."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Call the same read tool with valid, expired, wrong-issuer, wrong-audience, and missing-scope tokens.",
          "expected": "Only the correctly issued and scoped token succeeds; invalid or expired tokens return 401."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Inspect client requests and reverse-proxy logs for access tokens in URLs or query strings.",
          "expected": "Tokens appear only in the Authorization header and are redacted from logs."
        }
      ],
      "evidence": [
        "auth-negative-test-report",
        "redacted-request-capture"
      ],
      "releaseGate": true,
      "limits": "A valid token does not replace object-level authorization inside each handler."
    },
    {
      "id": "MCP-AUTH-02",
      "title": "Reject token passthrough",
      "appliesTo": [
        "server",
        "proxy"
      ],
      "risk": "The MCP server forwards a caller token to an upstream API or accepts an upstream token as its own credential.",
      "control": "Accept only tokens issued for the MCP server. Exchange or retrieve a separate downstream credential when an upstream API call is required, and keep the two trust boundaries distinct.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "official-guidance",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#token-passthrough",
          "requirementLevel": "MUST_NOT",
          "summary": "MCP servers do not accept tokens that were not explicitly issued for the MCP server."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Send a valid upstream API token directly to the MCP endpoint and capture the upstream request made by a valid MCP call.",
          "expected": "The direct upstream token is rejected, and the valid call uses a separate downstream credential."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Trace credential loading and forwarding paths from the HTTP boundary to every downstream adapter.",
          "expected": "No code path forwards the caller bearer value to a downstream service."
        }
      ],
      "evidence": [
        "credential-flow-diagram",
        "upstream-header-test"
      ],
      "releaseGate": true,
      "limits": "A token exchange still needs narrow downstream scopes, rotation, and revocation."
    },
    {
      "id": "MCP-OAUTH-01",
      "title": "Bind each OAuth response to its request",
      "appliesTo": [
        "client",
        "proxy"
      ],
      "risk": "An authorization response is replayed, mixed up with another issuer, or delivered to an unregistered redirect URI.",
      "control": "Store the expected issuer, PKCE verifier, state, client identity, and redirect URI per authorization attempt. Compare exact values at the callback, consume state once, and reject mismatches before token exchange.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "specification",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#authorization-response-validation",
          "requirementLevel": "MUST",
          "summary": "Clients record the validated issuer with request state and validate the authorization response before token exchange."
        },
        {
          "publisher": "MCP",
          "sourceType": "official-guidance",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#confused-deputy-problem",
          "requirementLevel": "MUST",
          "summary": "Proxy OAuth flows use exact redirect matching, protected state, and callback validation."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Replay a callback and vary state, issuer, redirect URI, and PKCE verifier one field at a time.",
          "expected": "Every mismatch and replay is rejected before a code reaches the token endpoint."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Inspect storage for pending authorization attempts and the lifecycle of callback state.",
          "expected": "State is random, short-lived, single-use, and bound to the same client request."
        }
      ],
      "evidence": [
        "oauth-callback-negative-tests",
        "pending-auth-record-review"
      ],
      "releaseGate": true,
      "limits": "These checks do not establish whether requested scopes are appropriate for the user."
    },
    {
      "id": "MCP-PROXY-01",
      "title": "Require consent per user and client",
      "appliesTo": [
        "proxy",
        "host"
      ],
      "risk": "A proxy reuses a trusted user's third-party session for a client the user never approved.",
      "control": "Record consent for the specific user and client before starting third-party authorization. Show the client name, requested scopes, and exact redirect URI in a clickjacking-protected consent view.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "official-guidance",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#confused-deputy-problem",
          "requirementLevel": "MUST",
          "summary": "MCP proxy servers implement per-client consent before third-party authorization."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "exercise",
          "procedure": "Authorize client A, then attempt the same flow for client B while the third-party login session remains active.",
          "expected": "Client B receives a new MCP consent screen and cannot inherit client A consent."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Review the consent screen and storage key using two users, two clients, and two redirect URIs.",
          "expected": "The decision is bound to user and client, and the screen displays exact scopes and redirect URI."
        }
      ],
      "evidence": [
        "consent-isolation-exercise",
        "consent-ui-capture"
      ],
      "releaseGate": true,
      "limits": "Consent records need their own expiry, revocation, CSRF, and cookie protections."
    },
    {
      "id": "MCP-NET-01",
      "title": "Constrain OAuth discovery requests",
      "appliesTo": [
        "client",
        "proxy",
        "operator"
      ],
      "risk": "Malicious metadata points OAuth discovery or redirect requests at internal services or cloud metadata endpoints.",
      "control": "Require HTTPS in production, block private and reserved destinations, validate every redirect hop, account for DNS rebinding, and route server-side discovery through an egress policy.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "official-guidance",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#server-side-request-forgery-ssrf",
          "requirementLevel": "MUST",
          "summary": "Server-side MCP clients consider SSRF risk and apply suitable controls to OAuth-related URL fetches."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Serve metadata pointing to loopback, RFC 1918, link-local, IPv6-private, encoded-IP, and redirect-chain targets.",
          "expected": "Every production request is blocked before reaching the protected destination."
        },
        {
          "id": "V-02",
          "type": "exercise",
          "procedure": "Run the discovery path through the production egress layer and review its destination and DNS decision logs.",
          "expected": "Only approved HTTPS destinations leave the workload, with redirect hops checked separately."
        }
      ],
      "evidence": [
        "ssrf-fixture-results",
        "egress-policy-export"
      ],
      "releaseGate": true,
      "limits": "Address checks alone are weak; use maintained parsers and network enforcement as defense in depth."
    },
    {
      "id": "MCP-SCOPE-01",
      "title": "Start with narrow scopes",
      "appliesTo": [
        "client",
        "server",
        "operator"
      ],
      "risk": "A stolen baseline token immediately enables unrelated read, write, and administrative tools.",
      "control": "Request a small baseline scope set, challenge for the exact privileged operation when needed, accept down-scoped tokens, and keep wildcard or omnibus scopes out of normal releases.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "official-guidance",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#scope-minimization",
          "requirementLevel": "GUIDANCE",
          "summary": "Official MCP guidance recommends a progressive least-privilege scope model instead of broad up-front access."
        },
        {
          "publisher": "OWASP",
          "sourceType": "community-guidance",
          "revision": "retrieved-2026-08-26",
          "url": "https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html#1-principle-of-least-privilege",
          "requirementLevel": "GUIDANCE",
          "summary": "Each server gets minimum permissions, scoped credentials, and short-lived tokens."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Call a privileged tool with the baseline token, then repeat after the documented step-up flow.",
          "expected": "The baseline token fails with a precise scope challenge; the elevated token enables only the intended operation."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Compare every published scope with the tools and resources it authorizes.",
          "expected": "No wildcard scope or unrelated privilege bundle remains without a dated exception."
        }
      ],
      "evidence": [
        "scope-to-tool-map",
        "step-up-flow-test"
      ],
      "releaseGate": true,
      "limits": "Scope names do not replace resource-level policy or user confirmation."
    },
    {
      "id": "MCP-OBJECT-01",
      "title": "Authorize state handles on every use",
      "appliesTo": [
        "server"
      ],
      "risk": "A caller guesses or obtains a workflow, cart, or job handle and uses another principal's state.",
      "control": "Generate non-deterministic handles, bind them server-side to identity derived from the verified token, enforce expiry, and authorize the resource and action on every request.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "official-guidance",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#state-handle-hijacking",
          "requirementLevel": "MUST_NOT",
          "summary": "Servers do not treat possession of a state handle as authentication."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Create state as principal A and present the handle with principal B for read, update, and delete operations.",
          "expected": "Every cross-principal operation is denied without revealing whether the handle exists."
        },
        {
          "id": "V-02",
          "type": "automated",
          "procedure": "Try expired, malformed, sequentially adjacent, and client-supplied identity variants of a valid handle.",
          "expected": "All variants fail, and authorization uses server-derived identity rather than caller fields."
        }
      ],
      "evidence": [
        "cross-principal-test-report",
        "handle-generation-review"
      ],
      "releaseGate": true,
      "limits": "Handle secrecy can reduce exposure but is never the authorization decision."
    },
    {
      "id": "MCP-TOOL-01",
      "title": "Treat tool metadata as untrusted",
      "appliesTo": [
        "client",
        "host",
        "server"
      ],
      "risk": "A description, annotation, schema field, or tool-name collision changes model behavior or hides a dangerous action.",
      "control": "Validate strict input and output schemas, identify the source server, disambiguate collisions, and keep annotations out of authorization decisions unless the server is trusted by policy.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "specification",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/specification/2026-07-28/server/tools#tool",
          "requirementLevel": "MUST",
          "summary": "Clients consider tool annotations untrusted unless they come from trusted servers."
        },
        {
          "publisher": "OWASP",
          "sourceType": "community-guidance",
          "revision": "retrieved-2026-08-26",
          "url": "https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html#2-tool-description-schema-integrity",
          "requirementLevel": "GUIDANCE",
          "summary": "Review and pin the whole tool definition, and use strict schemas for parameters."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Inject extra properties, malformed values, hostile descriptions, forged annotations, and colliding tool names.",
          "expected": "Schemas reject invalid arguments, and metadata cannot grant permission or silently replace another server's tool."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Diff the dated tool catalog against the release candidate, including descriptions, schemas, annotations, and source identity.",
          "expected": "Every change has an owner and security disposition before release."
        }
      ],
      "evidence": [
        "tool-catalog-diff",
        "schema-negative-tests"
      ],
      "releaseGate": true,
      "limits": "A valid schema constrains shape, not the business meaning or authorization of a value."
    },
    {
      "id": "MCP-APPROVAL-01",
      "title": "Keep sensitive approval outside the model",
      "appliesTo": [
        "client",
        "host"
      ],
      "risk": "Prompt injection or an accidental model choice triggers a destructive, financial, or data-sharing action.",
      "control": "Classify side effects in deterministic policy. Show the selected tool, destination, and full material arguments, then require a user decision through UI the model cannot operate.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "specification",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/specification/2026-07-28/server/tools#user-interaction-model",
          "requirementLevel": "SHOULD",
          "summary": "Applications keep a human able to deny calls and present confirmation for operations."
        },
        {
          "publisher": "OWASP",
          "sourceType": "community-guidance",
          "revision": "retrieved-2026-08-26",
          "url": "https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html#4-human-in-the-loop-for-sensitive-actions",
          "requirementLevel": "GUIDANCE",
          "summary": "Sensitive actions show full arguments and require explicit confirmation outside model control."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "exercise",
          "procedure": "Return tool content that instructs the model to approve a sensitive call and attempt UI automation through model output.",
          "expected": "The call remains blocked until the user acts in the trusted approval surface."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Compare approval text and displayed arguments with the exact request sent after approval.",
          "expected": "The user sees all material values, and any post-approval mutation invalidates consent."
        }
      ],
      "evidence": [
        "approval-bypass-exercise",
        "approval-request-binding-test"
      ],
      "releaseGate": true,
      "limits": "User approval does not make an unauthorized or invalid operation safe."
    },
    {
      "id": "MCP-OUTPUT-01",
      "title": "Validate and isolate tool output",
      "appliesTo": [
        "client",
        "host",
        "server"
      ],
      "risk": "A tool result carries instructions, raw HTML, commands, URLs, or secrets into the model and a later tool call.",
      "control": "Validate structured output against the declared schema, remove disallowed active content, label provenance, keep data separate from instructions, and apply downstream URL and command policies again.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "specification",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/specification/2026-07-28/server/tools#security-considerations",
          "requirementLevel": "MUST",
          "summary": "Servers validate tool inputs, enforce access, rate limit invocations, and sanitize tool outputs."
        },
        {
          "publisher": "OWASP",
          "sourceType": "community-guidance",
          "revision": "retrieved-2026-08-26",
          "url": "https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html#12-prompt-injection-via-tool-return-values",
          "requirementLevel": "GUIDANCE",
          "summary": "Treat tool responses as untrusted input and separate returned data from instructions."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Return schema violations, instruction-like text, raw active markup, internal URLs, and secret-shaped canary values.",
          "expected": "Invalid data is rejected or isolated, canaries stay out of later calls, and protected URLs are not fetched."
        },
        {
          "id": "V-02",
          "type": "exercise",
          "procedure": "Run a fixed multi-tool scenario where the first tool result asks the model to exfiltrate data through the second tool.",
          "expected": "The second call is denied or requires independent policy and approval checks."
        }
      ],
      "evidence": [
        "hostile-output-fixture",
        "cross-tool-exfiltration-exercise"
      ],
      "releaseGate": true,
      "limits": "String filtering alone cannot identify every prompt injection or unsafe semantic action."
    },
    {
      "id": "MCP-LOCAL-01",
      "title": "Contain local server execution",
      "appliesTo": [
        "client",
        "host",
        "local-server",
        "operator"
      ],
      "risk": "A local MCP package or startup command reads host data, opens a listener, or executes with all client privileges.",
      "control": "Show the exact command before installation, require consent, prefer stdio, and launch the process with explicit file, network, environment, and OS restrictions. Grant additional access separately.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "official-guidance",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices#local-mcp-server-compromise",
          "requirementLevel": "MUST",
          "summary": "One-click local configuration shows the exact command and requires explicit user consent before execution."
        },
        {
          "publisher": "OWASP",
          "sourceType": "community-guidance",
          "revision": "retrieved-2026-08-26",
          "url": "https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html#3-sandbox-and-isolate-mcp-servers",
          "requirementLevel": "GUIDANCE",
          "summary": "Local servers run with restricted file and network access in an isolation boundary."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "exercise",
          "procedure": "Use a harmless canary server that attempts undeclared file reads, outbound network access, child processes, and listener creation.",
          "expected": "Every undeclared capability is blocked and recorded without exposing the canary content."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Compare the displayed install command and declared permissions with the actual process invocation and sandbox policy.",
          "expected": "The command is not truncated or rewritten, and runtime access matches the approved set."
        }
      ],
      "evidence": [
        "sandbox-canary-report",
        "install-consent-capture"
      ],
      "releaseGate": true,
      "limits": "Containers reduce exposure but are not a complete boundary without current runtime and kernel controls."
    },
    {
      "id": "MCP-AUDIT-01",
      "title": "Record decisions without leaking data",
      "appliesTo": [
        "client",
        "server",
        "proxy",
        "host",
        "operator"
      ],
      "risk": "An incident cannot be reconstructed, or logs become a second copy of credentials and personal data.",
      "control": "Record actor, tenant, tool, policy decision, result class, latency, release identity, and correlation ID. Omit or redact tokens, raw personal data, and sensitive tool arguments before storage.",
      "sources": [
        {
          "publisher": "MCP",
          "sourceType": "specification",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/specification/2026-07-28/server/tools#security-considerations",
          "requirementLevel": "SHOULD",
          "summary": "Clients log tool usage for audit while servers enforce access and invocation controls."
        },
        {
          "publisher": "OWASP",
          "sourceType": "community-guidance",
          "revision": "retrieved-2026-08-26",
          "url": "https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html#10-monitoring-logging-auditing",
          "requirementLevel": "GUIDANCE",
          "summary": "Monitor tool use and redact secrets and personal data from audit logs."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Send canary tokens, emails, free text, and sensitive arguments through allowed and denied calls, then scan all log sinks.",
          "expected": "Required decision fields remain queryable, while every canary value is absent or irreversibly redacted."
        },
        {
          "id": "V-02",
          "type": "exercise",
          "procedure": "Reconstruct one allowed call, one denied call, and one timeout using only retained audit records.",
          "expected": "The reviewer identifies actor, release, tool, decision, and outcome without opening raw payloads."
        }
      ],
      "evidence": [
        "log-redaction-test",
        "incident-reconstruction-note"
      ],
      "releaseGate": true,
      "limits": "Retention, access, deletion, and residency rules remain deployment-specific."
    },
    {
      "id": "MCP-SUPPLY-01",
      "title": "Pin the server and tool catalog",
      "appliesTo": [
        "client",
        "host",
        "operator"
      ],
      "risk": "A package, publisher, command, schema, or description changes after approval and expands what the server can do.",
      "control": "Record package identity, source, version, integrity value, startup command, and normalized tool catalog. Block silent drift and require review when code, permissions, or tool definitions change.",
      "sources": [
        {
          "publisher": "OWASP",
          "sourceType": "community-guidance",
          "revision": "retrieved-2026-08-26",
          "url": "https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html#9-supply-chain-security",
          "requirementLevel": "GUIDANCE",
          "summary": "Verify server sources and integrity, review dependencies, and monitor tool-definition changes."
        },
        {
          "publisher": "MCP",
          "sourceType": "specification",
          "revision": "2026-07-28",
          "url": "https://modelcontextprotocol.io/specification/2026-07-28/server/tools#list-changed-notification",
          "requirementLevel": "SHOULD",
          "summary": "Servers that advertise list changes notify subscribed clients when the available tool catalog changes."
        }
      ],
      "verification": [
        {
          "id": "V-01",
          "type": "automated",
          "procedure": "Change one description, schema field, annotation, binary integrity value, and startup argument in separate fixtures.",
          "expected": "Every change produces a visible diff and prevents silent reuse of the previous approval."
        },
        {
          "id": "V-02",
          "type": "review",
          "procedure": "Resolve the installed package and publisher from a clean environment and compare them with the release record.",
          "expected": "Source, version, integrity value, command, and catalog match the reviewed release."
        }
      ],
      "evidence": [
        "server-release-record",
        "tool-catalog-drift-test"
      ],
      "releaseGate": true,
      "limits": "Integrity proves which artifact ran, not that the reviewed artifact is free of malicious behavior."
    }
  ]
}
