Repository navigation
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.
| 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. |
| 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
|
| 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 |
| 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 |
| 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 |
| 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
|
| 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
|
| 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 |
- An external security review of the TLS code. The internal review is in
docs/security/tls-review.mdin the repository. - The encrypted transfer of the Secret Service (
dh-ietf1024-sha256-aes128-cbc-pkcs7). The backend usesplainon 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.
- MCP-SPEC-2026-07-28: Model Context Protocol Specification 2026-07-28.
- MCP-SECURITY-BP-2026-07-28: Security best practices.
- MCP-HTTP-2026-07-28: Streamable HTTP transport, the security warning.
- RFC6750: Bearer Token Usage.
- RFC9449: OAuth 2.0 DPoP.
zig-sdk uses the Apache License 2.0. The wiki uses the project profile of ASD-STE100 Simplified Technical English. The Bibliography lists the sources.
zig-sdk
- Home
- Getting Started
- Concepts
- Server Guide
- Client Guide
- Transports
- Transport stdio
- Transport Streamable HTTP
- Transport gRPC
- Transport Unix Socket
- Transport WebSocket
- TLS and Certificates
- Deploy Reverse Proxy
- Authorization
- Authorization Server
- JSON Schema Support
- Resource Templates
- Tasks Extension
- Skills Extension
- Apps Extension
- Conformance
- Examples
- API Reference
- Interoperability
- Threat Model
- Limits and Security
- Deprecated Features
- Roadmap
- Release Notes
- Versioning
- Contributing
- FAQ
Style and words