Skip to content

Threat Model

Christian Presley edited this page Oct 8, 2026 · 19 revisions

Threat Model

This page maps the security requirements of the specification1 and its security guidance2 to the modules and tests of the SDK. The repository file SECURITY.md tells you how to report a vulnerability.

Applies to: MCP 2026-07-28, zig-sdk 0.1.0. The rows of the Unix socket, the regular expressions, the extensions, the icons, RSA, X25519MLKEM768 and the certificate checks apply to zig-sdk 0.2.0. The rows of WebSocket, the token storage, the authorization server, the timeouts, the cancellation and the header values apply to the same release. The rows of the principal binding, the URL checks, the cache scope, gRPC, the TLS records and the message lengths apply to the same release. The rows of the HTTP time limits, the JSON depth and the filter size apply to zig-sdk 0.3.0. The rows of the client memory, the client router, the proxy, HTTP/2, the shared storage, the CA bundle, the stdio process and _meta: zig-sdk 0.4.0.

Secure defaults

Setting Default Why
HTTP bind address 127.0.0.1 A local server is not reachable from the network without an explicit address.
Origin and Host checks Loopback only Protection against DNS rebinding3.
Request state Sealed with AES-256-GCM, 600 s lifetime, bound to the principal The client can change requestState, and another user can replay it.
Cache hints ttlMs: 0, cacheScope: private The client caches nothing across users without an explicit choice.
Client cache Off. Private results stay with one credential. A cached result of one user never goes to another user.
Request timeout 60 s, at most 600 s, not extended by progress A server that never answers cannot block the client for all time.
Body and line limits 4 MiB per HTTP body, 16 MiB per stdio line Bounded memory per request.
Response limits of the HTTP client 4 MiB per JSON body and per SSE event, 256 bytes per SSE event ID Bounded memory per message, also on a long stream.
Proxy of the HTTP client and the OAuth client None, unless the application gives an environment or a proxy URL For an https URL, a proxy sees only the host and the port of the server. The credentials of a proxy URL go only to the proxy.
Schema validation Every tool call, no network $ref A schema cannot make the server fetch a URL.
TLS TLS 1.3 only, no resumption, no early data The smallest safe protocol surface.
TLS key exchange X25519MLKEM768 first Recorded TLS traffic stays secret also against a future quantum computer.
Unix socket file Mode 0600, or on Windows an access control list for the owner only Only the owner can connect on Linux and on Windows.
Unix socket directory A private directory only, and a protected move of the socket Another account cannot put its own socket at the path. No client connects before the socket has its protection.
Icons https on the server origin and data: only, no credentials, no SVG An icon cannot make the client send a request to another host or send its credentials.
Rate limits Off The SDK keeps the behavior of existing servers. The application sets the limits for its traffic.
Tokens Authorization: Bearer, or Authorization: DPoP with a proof for a DPoP-bound token. Never logged. Bearer token usage rules4. A DPoP-bound token never goes out as a bearer token5.

Transport threats

Requirement Module Test
Validate Origin, answer 403 transport/http.zig http ladder and json responses
Verify Mcp-* headers against the body, answer -32020 transport/envelope.zig verify headers against body, conformance http-header-validation
Decode the base64 sentinel with a strict alphabet and padding transport/envelope.zig sentinel encoding, conformance http-custom-header-server-validation
Answer -32022 for an unsupported version server/Server.zig conformance server-stateless
Reject control characters, bytes that are not ASCII and unsafe integers in mirrored headers, answer -32020 transport/envelope.zig streamable http server rejects invalid characters and unsafe integers in mirrored headers
Close of the HTTP stream cancels the request, also when the handler writes nothing transport/http.zig streamable http server treats a closed SSE stream as the cancellation of the request, streamable http server cancels a handler that only polls when the client disconnects
A server sends nothing for a request that the client canceled transport/stdio.zig, transport/http.zig stdio server writes nothing for a cancelled request whose handler ignores the cancellation
A client cancellation with any reason text never looks like a server shutdown transport/Transport.zig, server/Server.zig only the first cancellation counts, and only shutdown marks the server as the origin, stdio server takes a client cancellation with the reason server shutdown as a client cancellation
Valid JSON that is not a message gets -32600 and never makes the server read freed memory transport/stdio.zig stdio server answers valid JSON that is not a message with Invalid Request and the id, unix socket server answers valid JSON that is not a message with the recovered id
A notification with an error member in its data never ends a request transport/router.zig only top-level keys make a frame a response
Only MCP on standard output transport/stdio.zig stdio client drives the example server process
A client excludes tools with invalid header annotations transport/http_client.zig conformance http-invalid-tool-headers
A header annotation is valid only on a property that a chain of properties keywords reaches transport/envelope.zig, transport/tool_headers.zig x-mcp-header annotations must be reachable through a chain of properties keywords, the client removes tools with unreachable x-mcp-header annotations, logs them and mirrors nested values
A Unix socket path never overflows the sockaddr_un of the target transport/unix.zig a socket path longer than sockaddr_un gives NameTooLong
The Unix socket file has the mode 0600 on POSIX transport/unix.zig unix socket file has mode 0600 on POSIX
The Unix socket file allows only the owner on Windows transport/unix.zig, transport/windows_acl.zig unix socket file allows only the owner on Windows
The Unix socket server refuses a directory where other accounts can add, delete or rename entries transport/unix.zig, transport/socket_dir.zig, transport/windows_acl.zig unix socket server refuses a directory that others can write to on POSIX, unix socket server refuses a directory with a parent that others can rename it in, unix socket server refuses a directory that others can write to on Windows
createPrivateDirectory makes a directory for the user only transport/socket_dir.zig, transport/windows_acl.zig createPrivateDirectory gives mode 0700 on POSIX, createPrivateDirectory gives an owner-only list on Windows
The socket moves to its path only after it has its protection transport/unix.zig unix socket server leaves only the socket in its directory
The Unix socket server never replaces a file that is not a socket or the socket of a live server transport/unix.zig unix socket server refuses a path that is not a socket, unix socket server refuses a live socket, replaces a stale one and removes its own
A closed Unix socket connection cancels only its own requests transport/unix.zig, transport/stdio.zig unix socket server cancels the requests of a closed connection
A grpc-timeout value in hours, such as 99999999H, does not cause an integer overflow grpc/grpc/timeout.zig parse and format
An HPACK literal with the name of the entry that it evicts never makes the decoder read freed memory grpc/http2/hpack/hpack.zig a literal whose indexed name the new entry evicts keeps the name
A typed gRPC message has a limit for its nesting and for its elements grpc/protobuf/codec.zig codec refuses malformed input
A slow client cannot keep an HTTP connection without end: a late head or body gets HTTP 408, and an idle connection closes transport/http.zig http limits: a request head that does not arrive in head_timeout gets 408, http limits: the server closes a keep-alive connection after idle_timeout without a response, http limits: a request body that does not arrive in idle_timeout gets 408
The HTTP server shutdown does not wait for a peer that sends nothing transport/http.zig http shutdown ends a connection that sends nothing, also without time limits
A deeply nested message gets -32700 before the parse and cannot exhaust the stack json.zig, jsonrpc/message.zig, transport/http.zig, transport/stdio.zig the depth check counts brackets outside strings, parseTree rejects a text that std.json.Stringify cannot write, stdio server answers a message deeper than limits.json_max_depth with a parse error, http limits: a message deeper than json_max_depth gets -32700
One SSE event has a size limit, and a long event ID cannot make each event use a copy of it transport/sse.zig, transport/http_client.zig parser rejects an event over max_event_bytes that comes in many feeds, parser ignores a long event ID, thus many small events after it use little memory, http client: one SSE event or a JSON body over max_response_bytes ends the exchange with error.InvalidFrame
A response over the size limit fails the request, and the client does not send the request again transport/http_client.zig, client/Client.zig http client: the client does not send an idempotent request again after a JSON body over max_response_bytes
A frame that the client cannot read makes its request fail at once, and the client cancels the request on the server transport/router.zig, util/line_framer.zig, transport/stdio.zig a frame that the router cannot read makes the request with its id fail, an error response with a null id does not fail a request that got a frame, a frame that the stdio client cannot read makes its request fail at once
A log line of the client for a dropped frame has only the reason, the request id and the error code. A log line has no control character from a header of the server. transport/router.zig, transport/http_client.zig the warning of a dropped frame has the reason and a short request id only, the log text of a header value of the server has no control characters
For an https URL, a proxy sees only the host and the port of the server. The proxy gets the credentials of its URL only in the CONNECT request. transport/proxy.zig, transport/http1.zig the HTTP client reaches an HTTPS server through a CONNECT proxy with Basic credentials, the CONNECT request and its answer
A proxy from the environment never gets a loopback host. The Proxy header of a request to a web server cannot set the proxy of an http URL. transport/proxy.zig loopback hosts, the proxy of the environment, the NO_PROXY match
The stdio client never signals a process ID after the reap, and it ends a server that closes its output but does not exit transport/stdio.zig a process that a restart spawns after kill started gets the kill signal, stdio client ends a server that closes its output and runs on
A gRPC response that the server sends before it reads the request message stays readable after the reset grpc/http2/Connection.zig grpc: a response before the request message stays readable after the reset, a complete response before the end of the request stays readable after the reset, a complete response before the end of the request stays readable after a CANCEL reset
A peer cannot fill the concurrent streams of an HTTP/2 connection with requests that the server refuses grpc/http2/Connection.zig a refused request frees its stream and keeps the HPACK table of the connection
A size error of the HPACK decoder keeps the dynamic table in sync with the peer grpc/http2/hpack/hpack.zig a size error keeps the dynamic table the same as the table of the encoder
A header block on a stream that the server reset does not end the connection. A block on a stream that the client did not open ends it. grpc/http2/Connection.zig a header block on a stream that this side reset is ignored, a header block on a stream id that this side did not reset ends the connection, the connection remembers the last remembered_resets streams that it reset
A gRPC call that stops does not keep its stream or its task, and a header block over the limit cannot stop the connection grpc/http2/Connection.zig, grpc/transport/grpc_server.zig, grpc/transport/grpc_client.zig a cancel from another task wakes the task that waits on the stream, stopWaits ends each wait on the stream while the connection wakes its waiters, a cancel before the stream has an id keeps the stream from the peer, a header block over the limit gives ENHANCE_YOUR_CALM after the connection unlocks

Request threats

Requirement Module Test
A caller over its tool call limit gets -31429 before the server validates the arguments server/Server.zig, server/rate_limits.zig tool calls over the rate limit of a caller get -31429 with a retry time
The limit of one principal does not affect another principal server/rate_limits.zig, transport/http.zig the tool call limit of one principal does not affect another principal, resource server: rate limits keep principals apart and answer 429 with Retry-After
Many callers cannot make the buckets use more memory than max_callers allows util/rate_limit.zig the server keeps buckets for at most max_callers callers and forgets the least recent one, the caller table forgets the caller that it used least recently
Log messages over the limit of a caller do not go out, and the server reports the count server/RequestContext.zig log messages over the rate limit of a caller are dropped, counted and reported in one summary
Missing _meta fields answer -32602 server/Server.zig discover and ladder errors
A missing client capability answers -32021 server/Server.zig tools list and call
The server treats requestState as attacker-controlled server/request_state.zig multi round-trip request with sealed state, conformance input-required-result-tampered-state
Only the principal of the request that sealed requestState can open it server/request_state.zig, server/Server.zig sealed state is bound to the principal, resource server: sealed request state opens only for the principal that it was sealed for
A URL elicitation has a valid absolute URL server/mrtr.zig, client/Client.zig URL elicitation needs a valid absolute URL on the server and on the client
includeContext other than none needs the capability sampling.context server/Server.zig server uses includeContext values only when the client declared sampling.context
Completion for an unknown prompt or resource template answers -32602 server/Server.zig completion rejects an unknown prompt or resource template with invalid params
The server requests only declared input kinds server/mrtr.zig conformance input-required-result-capability-check
The server validates tool arguments schema/validator.zig tool schemas validate arguments and structured output
Structured output matches the output schema server/Server.zig the same test
Not found resources answer -32602 with the URI server/Server.zig conformance sep-2164-resource-not-found
Completion values are limited to 100 server/Server.zig resources, templates, prompts, completion
Progress notifications are rate limited per second, on the server and on the client server/RequestContext.zig, client/Client.zig, util/rate_limit.zig progress notifications of one request are rate limited per second, client drops the progress notifications of a request over the rate limit
A regular expression cannot cause catastrophic run time: the match time is linear in the input length schema/regex.zig pathological patterns run in linear time, limits
A plain client does not see or call the tools that only a view can call server/Server.zig, server/apps.zig apps: a client without the extension gets plain tools
A subscriptions/listen filter has a size limit server/Server.zig a listen filter above limits.max_filter_bytes gets -32603 and no stream

Authorization threats

Requirement Module Test
The resource server validates every token with its audience. The comparison ignores only the case of the scheme and the host. auth/jwt.zig, auth/common.zig, auth/resource_server.zig hs256 round trip and claims, challenges and decisions, canonical URI comparison
A broader scope satisfies a narrower scope only through the configured hierarchy auth/resource_server.zig scope hierarchy
The server never passes tokens through transport/http.zig resource server: metadata, challenges and the principal
Challenges carry resource_metadata and scope auth/resource_server.zig the same tests
PKCE S256, refusal without it auth/oauth_client.zig conformance auth/*
iss compared without normalization auth/oauth_client.zig conformance auth/iss-normalized
Credentials bound to one issuer auth/oauth_client.zig conformance auth/authorization-server-migration, client registers again and keeps no credentials of the old authorization server after a change
Pre-registered credentials go only to the authorization server of their issuer auth/oauth_client.zig client uses the pre-registered credentials of each issuer and fails for an unknown issuer, client binds pre-registered credentials without an issuer to the first authorization server
The client refuses http metadata, registration, authorization and token endpoints auth/common.zig, auth/oauth_client.zig client refuses plain http metadata, registration, authorization and token endpoints by default
The redirect URI is https or a loopback http URI, and the client ID metadata document URL is https with a path auth/common.zig, auth/oauth_client.zig client refuses a redirect URI that is not https or a loopback URI, client refuses a metadata document URL without https or without a path
A bounded number of step-up attempts auth/oauth_client.zig conformance auth/scope-retry-limit
The resource of the metadata must cover the server URL auth/oauth_client.zig conformance auth/resource-mismatch
The SDK never opens authorization URLs auth/oauth_client.zig design: the callback mode gives the URL to the application
The authorization extensions send credentials only to https token endpoints auth/client_credentials.zig, auth/enterprise.zig design: allow_http is for tests only
An ID-JAG must have the right type, a trusted issuer, one audience, the authenticated client, a short lifetime and a new jti auth/enterprise.zig id-jag validation rules
A DPoP proof has the type dpop+jwt, an asymmetric algorithm and a public key. It names the method, the URI and the token hash, and its iat is within 5 minutes. auth/dpop.zig a proof verifies and binds to the token, proof headers that the verifier refuses
A DPoP-bound token works only with a proof of its key, and never with the Bearer scheme auth/resource_server.zig DPoP: bound tokens, proofs, nonces and the downgrade to bearer, DPoP: client credentials with DPoP-bound tokens and nonces at both servers
A server nonce limits the time in which a stolen proof is usable, and the server keeps no state for it auth/dpop.zig nonces: the issuer accepts its own nonces within the lifetime, conformance auth/dpop-nonce
A token claim near the limit of i64 or a large float cannot stop the process auth/jwt.zig time claims at the limits of i64 and large floats do not overflow
A workload JWT has a trusted issuer, a signature of a key of that issuer and the token endpoint as audience. Its lifetime is short, and the policy accepts its workload. auth/workload_identity.zig workload JWT validation rules
The workload identity client does not send a refused JWT again and does not change the grant type auth/workload_identity.zig workload identity: a workload JWT as a grant, with keys from OpenID Connect Discovery, conformance auth/wif-jwt-bearer
Key discovery for workload JWTs uses only https and checks the issuer of the configuration auth/workload_identity.zig design: KeyDiscovery.fetchJwks
With ca_bundle, the OAuth client verifies each server against the bundle only, and std.http.Client never gets the bundle auth/common.zig, auth/oauth_client.zig the fetcher verifies a server against its ca_bundle only, and the HTTP client of std never gets the bundle, authorization code flow over HTTPS with the CA of the ca_bundle option
Through a proxy, the OAuth client speaks TLS to the server inside the tunnel auth/common.zig, transport/proxy.zig authorization code flow over HTTPS through a CONNECT proxy

TLS threats

Requirement Module Test
The server refuses TLS 1.2 and older tls/handshake/server.zig a TLS 1.2 client hello is refused with protocol_version
The handshake rejects low-order and invalid peer keys tls/handshake/key_share.zig rejects malformed and low-order peer keys
Records have a size limit, and the connection checks the padding tls/Connection.zig encrypted record round trip
A record that does not fit after the unread data never goes out of the bounds of the buffer tls/Connection.zig a record that does not fit after unread data arrives in order, fuzz: TLS record layer with padding, partial input and one changed byte
A length in a Certificate or CertificateRequest message near the maximum of its type is a decode error tls/handshake/common.zig certificate message lengths near the maximum of their type are decode errors
The SDK wipes key material tls/suites.zig, tls/PrivateKey.zig deinit and wipe functions
Handshake messages have a limit of 16 KiB tls/handshake/server.zig MessageReader limit
An RSA key verifies each signature before it goes out, so a fault in the private operation cannot leak the key tls/rsa.zig design: signEncoded returns error.SigningFailed on a mismatch
The RSA key loader refuses weak and inconsistent keys tls/rsa.zig key parsing refuses bad keys
The hybrid key exchange checks the lengths, the ML-KEM encapsulation key and the X25519 result tls/handshake/key_share.zig hybrid layout: ML-KEM first, then X25519, rejects malformed and low-order peer keys
The client refuses a HelloRetryRequest cookie larger than 8 KiB tls/handshake/client.zig design: max_cookie_len
A certificate authenticates only the role that its extended key usage permits tls/verify.zig, tls/x509.zig the extended key usage of the leaf and the intermediates must permit the purpose, the extended key usage of the peer must fit its role
Each intermediate has basic constraints with cA set tls/verify.zig an intermediate needs basic constraints with cA, an anchor can lack them
The host name matches only the subject alternative name, and wildcards cover one full label tls/verify.zig, tls/x509.zig the host name check ignores the common name, host name matching follows RFC 6125 and refuses broad wildcards
The names of each certificate agree with the name constraints of each CA above it tls/name_constraints.zig, tls/verify.zig name constraints of two CAs intersect and an anchor can have them too, the client enforces the name constraints of the CA
A critical name constraint that the SDK cannot check fails closed tls/name_constraints.zig a constraint form that the SDK cannot check fails only when critical
A revoked certificate ends the handshake, and hard_fail refuses an unknown status tls/revocation.zig, tls/Crl.zig a CRL refuses a revoked leaf, and the policy decides about an unknown status, a CRL from the application refuses a revoked certificate on both sides
A CRL or an OCSP response counts only with a valid signature of the issuer or of a delegated responder tls/Crl.zig, tls/ocsp.zig the CRL signature verifies with its issuer only, OCSP responses that the client refuses
A bad OCSP staple ends the handshake, also with soft_fail tls/revocation.zig, tls/handshake/client.zig a stapled OCSP response gives the status, and the client refuses a bad one
The ClientHello parser refuses duplicate extensions and bytes after the structure of an extension tls/handshake/codec.zig a duplicate extension after many other extensions is refused, trailing bytes in a known client hello extension are a decode error
A malformed CA name list is a decode error tls/handshake/ca_names.zig malformed name lists are decode errors

Client threats

Requirement Module Test
Icons use only https and data: client/icons.zig icon schemes
An https icon has the origin of the server, also after a redirect client/icons.zig icon origins, icon fetch over https: headers, redirects and limits
The icon fetch sends no cookie and no Authorization header client/icons.zig icon fetch over https: headers, redirects and limits
The magic bytes of an icon agree with its declared type, and formats that can contain script need a decoder client/icons.zig magic bytes and dimensions, declared type precedence, decoder rule
The size, the dimensions and the time of an icon fetch have limits client/icons.zig icon fetch timeout, Client.fetchIcon: policy, limits and the decoder hook
The client verifies each skill file against the size and the digest of the skill entry protocol/skills.zig, client/Client.zig entry validation and verification, skills: the client lists, gets, reads and verifies
The frontmatter parser refuses YAML that other parsers can read differently protocol/skills.zig frontmatter: unsupported and invalid YAML fails closed
The client validates a form answer against requestedSchema client/Client.zig client validates the form answer against the requested schema
The client refuses sampling messages that break the tool result rules protocol/types.zig, client/Client.zig sampling message tool rules, sampling messages with tool results hold only tool results on the server and on the client
The client cache serves a private result only for the credential that got it client/cache.zig, client/Client.zig private entries belong to one authorization context, client keeps private cache entries apart by authorization context
The client mirrors only integers in the safe range of JavaScript transport/envelope.zig, transport/tool_headers.zig the client mirrors only integers in the safe range of JavaScript
The WebSocket server refuses an upgrade with a foreign Origin or Host, without the subprotocol mcp or with another version transport/websocket.zig websocket server refuses an upgrade that breaks a rule
A WebSocket peer that breaks a rule of RFC 6455 gets the close code of the rule transport/ws_frame.zig, transport/websocket.zig websocket server closes with the code of each protocol violation
The WebSocket server checks the token at the upgrade and closes the connection when the token expires transport/websocket.zig websocket authorization: challenge at the upgrade, metadata and the principal, websocket authorization: the server closes with 1008 when the token expires
The WebSocket limits bound the connections, the requests in flight, the message size and the handshake time transport/websocket.zig websocket limits: connections, requests in flight and the handshake time
A client cannot get more rate limit tokens with more WebSocket connections transport/websocket.zig websocket rate limits: the connections of one IP address share the bucket of the address
A value from a peer cannot add a header to a request of the SDK clients. Examples are an access token and a URL from metadata. util/http_syntax.zig, transport/http1.zig, transport/websocket.zig, auth/common.zig a request head with CR or LF in a part is refused, the fetcher refuses a URL or a header with CR or LF before it sends a request, a token response with an access token that is not a token68 value is a failure
Every request has a timeout, and progress does not extend it client/Client.zig client gives every request a default timeout and a maximum timeout, but not a listen stream, client cancels a request at the default timeout although progress keeps arriving
A listen stream without its acknowledgment ends at a time limit client/Client.zig, transport/Transport.zig http client: a listen stream without its acknowledgment in listen_ack_timeout ends with error.Timeout
A long listen stream does not make the memory of its request larger client/Client.zig a long listen stream over stdio does not make the request arena larger, a long listen stream over the memory link does not make the request arena larger
A key of RequestOptions.meta cannot replace a _meta key that the SDK owns client/Client.zig, protocol/meta.zig extra _meta entries reach the server and SDK-owned keys fail the request, the SDK owns its request keys and no other key of the prefix, the entries of the SDK replace extra _meta entries with the same key

Token storage threats

Requirement Module Test
A refresh never asks for a wider scope than the grant has, and a step-up needs a new grant auth/oauth_client.zig oauth client never asks for a wider scope with a refresh token, oauth client does not use the refresh token for a step-up
A rotated refresh token replaces the old one, and the client discards a refused refresh token auth/oauth_client.zig oauth client refreshes the access token before it expires and keeps the rotated refresh token, oauth client runs the authorization code flow when the server refuses the refresh token
A refresh of a DPoP-bound grant carries a proof, and the client skips stored tokens of another DPoP key auth/oauth_client.zig oauth client sends a DPoP proof with the refresh token, oauth client skips stored tokens of another DPoP key and keeps the registration
Clients that share one storage use the tokens that another client refreshed, and do not write over a record that another client changed auth/oauth_client.zig oauth clients that share a file storage use the tokens that the other client refreshed, oauth client takes the tokens of another process that rotated the refresh token during the refresh, oauth client removes refused tokens from the storage only when no other client changed the record, oauth client deletes the stored record after a refused client only when no other client changed the record
A record of another client ID with tokens of another DPoP key does not replace the registration and the refresh token of the client auth/oauth_client.zig oauth client keeps its registration and refresh token when a record of another client ID has tokens of another DPoP key
AES-256-GCM encrypts and authenticates each token file. The file name does not tell the issuer, and a moved or changed file does not decrypt. auth/token_storage.zig file token storage encrypts each record and detects a change
Only the user can read the token files: mode 0600 in a 0700 directory, or an owner-only list on Windows auth/token_storage.zig, transport/socket_dir.zig, transport/windows_acl.zig file token storage gives the owner only access to the directory and the files
A record works only for its key: the issuer, the resource and the client. A new authorization server deletes the record of the old one. auth/token_storage.zig, auth/oauth_client.zig a record serializes to versioned JSON and parses back for its key only, oauth client deletes the stored record when the issuer changes or the server refuses the client
The keychain backends erase the buffers with secrets, and the D-Bus client authenticates as the user of the process auth/keychain.zig, auth/keychain/*.zig d-bus client authenticates with EXTERNAL and keeps a signal of the bus, secret service backend keeps, replaces and deletes a record on a fake bus

Authorization server threats

Requirement Module Test
The server accepts only PKCE S256, and the verifier must match the challenge auth/authorization_server.zig PKCE: S256 only, and the verifier must match
A code is single use, and a second use revokes the refresh tokens of its grant auth/authorization_server.zig, auth/authorization_store.zig a code that comes a second time revokes the refresh token of its grant, the memory store: codes are single use and expire
Refresh tokens rotate, and a rotated token that comes again revokes its grant family auth/authorization_server.zig refresh tokens: rotation, narrower scopes and reuse detection
The server compares redirect URIs exactly, and never redirects to an unknown URI auth/authorization_server.zig redirect URIs: exact match, any loopback port, never a redirect to an unknown URI
The server compares client secrets in constant time, and the jti of a client assertion is single use auth/authorization_server.zig client authentication: secrets, assertions and their replay
A DPoP-bound code or refresh token works only with its key auth/authorization_server.zig DPoP: bound codes, bound refresh tokens, nonces and a proof of another key
The fetch of a client ID metadata document goes only to public https addresses, with a size and a time limit auth/client_metadata.zig the URL check and the refusal of private addresses, a server that does not answer reaches the time limit
The store keeps hashes of codes, refresh tokens and client secrets, not the values auth/authorization_store.zig the memory store: refresh tokens, family revocation and jti values

Not covered yet

  • An external security review of the TLS code. The internal review is in docs/security/tls-review.md in the repository.
  • The encrypted transfer of the Secret Service (dh-ietf1024-sha256-aes128-cbc-pkcs7). The backend uses plain on the local bus.
  • A lock between processes that share one token storage. A client reads the record again before a refresh and after a refused refresh, and uses the tokens of the other process. Two processes can send a refresh at the same time. With refresh token rotation, the server then refuses one of them. When the other process did not save its tokens yet, the refused process authorizes again.

The Roadmap lists the external review. See Limits and Security for the limits.

References

  1. MCP-SPEC-2026-07-28: Model Context Protocol Specification 2026-07-28.
  2. MCP-SECURITY-BP-2026-07-28: Security best practices.
  3. MCP-HTTP-2026-07-28: Streamable HTTP transport, the security warning.
  4. RFC6750: Bearer Token Usage.
  5. RFC9449: OAuth 2.0 DPoP.

Clone this wiki locally