This release includes 2 security fixes for security teams reviewing exposed deployments.
Topics
Affected surfaces
Summary
AI summaryis_token_expired now correctly returns true for expired tokens, fixing silent forwarding of expired bearer tokens.
Full changelog
[3.1.0] - 2026-04-17
This release lands the remediation pass from the v3.0.13 audit.
Five categories of fix: security correctness, transport correctness, protocol/macro correctness, CI/test coverage, extension-crate honesty markers.
Security
-
is_token_expiredis no longer a no-op —crates/turbomcp-auth/src/oauth2/client.rs:626. Pre-3.1 the checkexpires_in == 0treated a relative duration as a countdown clock; it never returnedtrue, so OAuth callers silently forwarded expired bearer tokens forever.TokenInfonow carriesissued_at: Option<SystemTime>(serde-default for back-compat with cached v3.0 token entries), populated byOAuth2Client::token_response_to_token_info. NewTokenInfo::expires_at,is_expired, andis_expired_with_skew(Duration)helpers;is_token_expireddelegates to them with a 60s clock skew. -
DPoP
athclaim is now enforced at the resource server (RFC 9449 §4.3) —crates/turbomcp-dpop/src/proof.rs. New publicProofContext { TokenEndpoint, ResourceServer }enum threaded throughvalidate_proof/parse_and_validate_jwt(BREAKING — see Migration). At a resource server, presenting an access token alongside a proof withoutathnow returnsDpopError::AccessTokenHashFailed. Pre-3.1 a stolen DPoP proof could be paired with a separately-issued access token, defeating sender-constraint binding. New regression testtest_resource_server_requires_ath_when_token_present. -
TLS certificate validation CVEs resolved —
Cargo.lockupdates foraws-lc-sys 0.38.0 → 0.40.0(RUSTSEC-2026-0044, RUSTSEC-2026-0048) andrustls-webpki 0.103.9 → 0.103.12(RUSTSEC-2026-0049, RUSTSEC-2026-0098, RUSTSEC-2026-0099). Affected every outbound HTTPS inturbomcp-auth(OIDC discovery, JWKS),turbomcp-transport, andturbomcp-client.cargo auditnow reports zero open advisories beyond the documentedpaste(compile-time-only) /proc-macro-error/randlow-impact entries. -
JwtValidator::newandMultiIssuerValidator::add_issuernow apply SSRF protection by default —crates/turbomcp-auth/src/jwt/validator.rs. Pre-3.1 these constructors performed unguarded HTTP fetches to the issuer-derived OIDC discovery URL. In multi-issuer setups where the issuer string comes from an attacker-controllable JWT payload, that was an SSRF. The default constructors now wrap anSsrfValidator::default()policy (blocks loopback, RFC 1918, link-local, cloud metadata). Newnew_unchecked/add_issuer_uncheckedopt-outs for test/dev against private OIDC providers. -
DPoP nonce tracker has a bounded capacity and inline cleanup —
crates/turbomcp-dpop/src/proof.rs.MemoryNonceTrackernow supportswith_capacity(usize)(default 1,000,000) with time-ordered eviction triggered at 80% high-water insidetrack_nonce. Pre-3.1 the map was unbounded with no automatic cleanup, andis_nonce_useddid an O(n) constant-time scan — both compound CPU+memory DoS vectors via unique-JTI flooding. The lookup is now O(1) hashed (server-generated nonces have no per-character secret to leak through hashmap timing). -
OAuth redirect URI no longer accepts
0.0.0.0—crates/turbomcp-auth/src/oauth2/client.rsandcrates/turbomcp-auth/src/oauth2/resource.rs.0.0.0.0is the bind-all unspecified address, not loopback, so a callback sent to it can be intercepted by any process on any interface (RFC 8252 §7.3 violation). Allowed loopback hosts are now exactly127.0.0.1,[::1], andlocalhost. -
API keys are no longer stored plaintext in memory —
crates/turbomcp-auth/src/providers/api_key.rs.ApiKeyProvidernow stores BLAKE3 digests as the map key; plaintext values are dropped at the end ofadd_api_keyand never retained. Lookup is O(1) over digests with constant-time hashing of the input.add_api_keynow returnsMcpResult<()>and rejects keys shorter thanMIN_API_KEY_LENGTHat insertion.list_api_keysremoved (digests can't be inverted to plaintext); replaced byapi_key_count(). -
PKCE verifier returned as
secrecy::SecretString—crates/turbomcp-auth/src/oauth2/client.rs:425.authorization_code_flownow returns(String, SecretString)instead of(String, String)so the verifier zeroes on drop and won't leak throughDebug/ log accidentally. (BREAKING — see Migration.) -
OAuth
statevalidation no longer leaks length through timing —crates/turbomcp-auth/src/oauth2/validation.rs.validate_oauth_statenow compares fixed-length SHA-256 digests withsubtle::ConstantTimeEq. Pre-3.1 raw strings were compared, andct_eqshort-circuits on length mismatch — a small length oracle.
Transport
-
HTTP server has graceful shutdown —
crates/turbomcp-server/src/transport/http.rs. Newrun_with_shutdown(handler, addr, config, graceful_shutdown)entry point;axum::serve(...).with_graceful_shutdown(shutdown_signal(...))waits for SIGINT and, on Unix, SIGTERM, then drains in-flight requests up to the configured timeout (max 60s).ServerBuilder::with_graceful_shutdown(Duration)is now actually wired through; pre-3.1 it was a stored-but-ignored knob and SIGTERM aborted in-flight responses. -
HTTP client constructor returns
Resultinstead of panicking —crates/turbomcp-http/src/transport.rs:303.StreamableHttpClientTransport::newnow returnsTransportResult<Self>, propagating the underlyingreqwest::Client::build()failure. (BREAKING — see Migration.) Pre-3.1 a bad TLS configuration (e.g., a malformed custom CA cert byte slice) would panic the calling process. -
HTTP endpoint discovery synchronizes via
oneshotinstead of a 500 mssleep—crates/turbomcp-http/src/transport.rs.connect()now awaits anendpoint_readyoneshot fired by the SSE task on the firstendpointevent, with a timeout bounded byconfig.timeout. Pre-3.1 a fixed 500 ms wait raced on slow networks / cold caches and the firstsend()could be routed to a stale endpoint. -
WebSocket outbound channels are bounded (DoS fix) —
crates/turbomcp-transport/src/axum/handlers/websocket.rs,axum/websocket_factory.rs. NewWS_OUTBOUND_CAPACITY = 1024constant; both handler paths usempsc::channel(...)instead ofmpsc::unbounded_channel(). A slow / hostile client can no longer drive the server out of memory by reading slower than messages arrive. The bidirectional dispatcher (websocket_bidirectional.rs::WebSocketDispatcher) takes a boundedSenderandawaitssend. Pong replies usetry_sendso a saturated buffer closes the connection rather than stalling the receive loop. -
STDIO no longer silently drops messages under backpressure —
crates/turbomcp-stdio/src/transport.rs:476. The reader task nowsend().awaits on the bounded message channel rather thantry_send-and-drop-on-full. Pre-3.1 a slow consumer caused silent message loss with only awarn!log; request/response correlation broke under load. -
TCP connections set
TCP_NODELAYafter accept and connect —crates/turbomcp-tcp/src/transport.rs. MCP messages are typically small and latency-sensitive; without disabling Nagle, each frame could wait up to 200 ms for coalescing. -
HTTP client exposes async
recv_async()—crates/turbomcp-http/src/transport.rs. New inherent method that awaits on both the POST response queue and the SSE stream viatokio::select!(biased toward responses). ComplementsTransport::receive, which is non-blocking by contract;receivedocs now call this out explicitly so client code picks the right primitive. -
SSE chunk reads are timeout-guarded —
crates/turbomcp-http/src/transport.rs.StreamableHttpClientConfig::sse_read_timeout(default 5 minutes) wraps eachstream.next()intokio::time::timeoutso a silent TCP half-open breaks the SSE task and lets the reconnect loop take over instead of stalling forever.
Protocol
-
ProtocolConfig::default()is now multi-version —crates/turbomcp-server/src/config.rs. The defaultsupported_versionsis nowProtocolVersion::STABLE.to_vec()instead of[LATEST]. Older clients (e.g. on 2025-06-18) are accepted and routed through the existingVersionAdapterinfrastructure. UseProtocolConfig::strict(version)to restore exact-match behavior. Pre-3.1 the default rejected every client not on the latest spec, even though the adapters existed. -
JSON-RPC error code range validated —
crates/turbomcp-protocol/src/jsonrpc.rs.JsonRpcError::newnow logs atracing::warn!for codes outside the JSON-RPC 2.0 server-error range (-32099..=-32000) and the standardized codes (-32700, -32600, -32601, -32602, -32603). NewJsonRpcError::with_validated_codeconstructor returnsErrfor out-of-range codes. Pre-3.1 anyi32was silently accepted, risking collision with future spec assignments. -
URLElicitationRequiredErrortype added —crates/turbomcp-protocol/src/types/elicitation.rs. Carriesurl,description,elicitation_idand a constantERROR_CODE = -32001. Servers that need URL-mode elicitation but receive a form-mode request can now signal it spec-conformantly. -
ResourceTemplate::new(name, uri_template)validates RFC 6570 structure at construction —crates/turbomcp-protocol/src/types/resources.rs. Newvalidate_uri_templatehelper rejects unbalanced braces and nested{...}. The publicuri_templatefield stays writable so wire-format deserialization still round-trips, but server-side construction now catches typos at build-time. -
VersionManager::with_default_versions()no longer hides anunwrap()—crates/turbomcp-protocol/src/versioning.rs:235. Replaced with anexpect("known_versions is non-empty by const construction")that names the contract. -
CompositeHandlerprefix matching no longer mis-splits prefixes containing_or://—crates/turbomcp-server/src/composite.rs.parse_prefixed_tool/parse_prefixed_uri/parse_prefixed_promptnow look up the matching mounted prefix (longest-first) instead ofsplit_once('_'). Pre-3.1 a prefix likemy_weathermounted with toolget_forecastwould fail to route because the joined namemy_weather_get_forecastsplit as("my", "weather_get_forecast"). -
CompositeHandler::mountvstry_mountclarified —crates/turbomcp-server/src/composite.rs. Rustdoc now steers new code attry_mount(returnsResulton duplicate prefix) and flagsmountas a candidate for v4 deprecation, while keeping it ergonomic for static setups (tests, examples, small servers with compile-time-known prefix sets).
Macros
-
#[tool]schema fallback no longer collapses scalar parameter types to{"type":"object"}—crates/turbomcp-macros/src/tool.rs:384. Whenschemars::schema_for!emits a non-object root schema (e.g.,{"type":"boolean"}forbool,{"anyOf":[..., {"type":"null"}]}forOption<T>), the fragment is now wrapped underallOfso it correctly describes the property instead of being silently replaced by a generic object schema. Pre-3.1, scalar-typed parameters appeared as opaqueobjects in the tool input schema, and LLM clients sent wrong-typed values. -
#[tool]optional-parameter parsing distinguishes "absent" from "present-but-malformed" —crates/turbomcp-macros/src/tool.rs:496. The previous.transpose().map_err(...)?.flatten()chain mishandled theOption<Option<T>>shape. Replaced with explicitmatch args.get(name). -
#[prompt]arguments now surface#[description("...")]—crates/turbomcp-macros/src/server.rs. Parameter descriptions are pulled from the#[description]attribute (mirroring the#[tool]extraction) and emitted intoPromptArgument.description. Pre-3.1 prompts always emitteddescription: None.
CI / Tests / Observability
-
Integration tests now run in CI —
.github/workflows/test.yml. NewIntegration testsandDoc testssteps runcargo test --workspace --all-features --testsand--docalongside the existing--lib --bins. Pre-3.1 ~600 integration / compliance / fault-injection tests intests/and per-cratetests/were never executed in CI. -
MSRV (1.89.0) verified in CI — new
msrvjob usingdtolnay/[email protected]runscargo check --workspace --all-features. Catches use of post-1.89 features that would break downstream consumers pinned to the declared MSRV. -
Phantom-API tests removed — deleted
tests/coverage_tests.rsandtests/external_dependency_integration.rs. Both referenced types and methods (StateManager,TransportType,ErrorKind::Transport,ctx.info(),into_mcp_router(),get_tools_metadata()) that no longer exist in the v3 API. Once--testsruns in CI they would fail to compile; equivalent coverage is in the current MCP compliance suites. -
Telemetry has a behavioral test — new
crates/turbomcp-telemetry/tests/behavioral.rs. DrivesTelemetryService::callend-to-end through a tower service stack and asserts that anmcp.requestspan fires with the expectedmcp.methodfield. Pre-3.1 the only telemetry tests asserted on the constant strings used as field names — they passed even when no spans were ever recorded. -
OriginConfigdefault documented as dev-only —crates/turbomcp-transport/src/security/origin.rs.Defaultreturnsallow_localhost: truefor development convenience; the doc comments now state explicitly that production deployments must override this. -
Fuzz workflow re-enabled —
.github/workflows/fuzz.yml. All four fuzz targets (fuzz_jsonrpc_parsing,fuzz_tool_deserialization,fuzz_message_validation,fuzz_capability_parsing) verified to compile against current types. Workflow runs onturbomcp-protocol-touching PRs (60 s per target) and nightly at 03:00 UTC (600 s per target), with corpus caching and crash artifact upload. Pre-3.1 the workflow was fully commented out because the targets had drifted. -
WASM macros have a
trybuildcompile-fail harness —crates/turbomcp-wasm-macros/tests/. Newtrybuilddev-dependency plus compile-fail snapshots for#[server]placed on non-impl syntactic shapes. Gives the crate a test harness that downstream integration tests can extend without requiring a fullturbomcp-wasmdependency closure.
Extension Crates
-
OpenAPI
$refreferences are resolved and inlined —crates/turbomcp-openapi/src/provider.rs.schema_to_jsonnow walks the converted schema and recursively expands#/components/schemas/*pointers into the emitted MCP tool / resource input schemas, with cycle detection that preserves the innermost$refso self-referential schemas stay finite.allOf,oneOf,anyOf,discriminator, andnullableround-trip through the serialization path as JSON Schema keywords that MCP clients speaking JSON Schema 2020-12 consume directly. README's former "Known Limitations" section is replaced with a positive description of what's supported. New tests:test_ref_resolution_inlines_components,test_ref_resolution_handles_cycles. -
Proxy
graphqlfeature removed fromadaptersbundle —crates/turbomcp-proxy/Cargo.toml. Thegraphqladapter was Phase 6 scaffolding with noasync-graphqldeps actually pinned; enabling the feature did not produce a working GraphQL adapter. Kept as a placeholder feature flag (so existing references don't break) but no longer included infeatures = ["adapters"]or["full"]. -
WASM WASI completeness clarified —
crates/turbomcp-wasm/README.md. New section noting WASI bindings cover stdio + HTTP only (no streaming, no WASI sockets), and that the browser target is the more mature one.
Breaking changes
See MIGRATION.md for 3.0.x → 3.1.0 upgrade notes. Summary:
TokenInfogainsissued_at: Option<SystemTime>(serde default; on-disk back-compat preserved).DpopProofGenerator::validate_proofandparse_and_validate_jwttake a newProofContextparameter.StreamableHttpClientTransport::newreturnsTransportResult<Self>instead ofSelf.OAuth2Client::authorization_code_flowreturns(String, secrecy::SecretString)instead of(String, String).ApiKeyProvider::add_api_keyreturnsMcpResult<()>and enforcesMIN_API_KEY_LENGTHat insert time.ApiKeyProvider::list_api_keysremoved (no plaintext available); useapi_key_count().JwtValidator::new/MultiIssuerValidator::add_issuernow apply a default SSRF policy. Usenew_unchecked/add_issuer_uncheckedfor test/dev against private OIDC providers.ProtocolConfig::default()is now multi-version. UseProtocolConfig::strict(LATEST)to restore the v3.0 single-version default.- OAuth loopback redirect URIs no longer accept
0.0.0.0. Use127.0.0.1,[::1], orlocalhost.
Full Changelog: https://github.com/Epistates/turbomcp/compare/v3.0.14...v3.1.0
Breaking Changes
- `StreamableHttpClientTransport::new` now returns `Result` instead of panicking
- `OAuth2Client::authorization_code_flow` returns `(String, secrecy::SecretString)` (BREAKING)
- `ProofContext { TokenEndpoint, ResourceServer }` added to DPoP validation APIs (BREAKING)
- `ApiKeyProvider::add_api_key` now returns `McpResult`, enforces minimum length; `list_api_keys` removed
- `JwtValidator::new` and `MultiIssuerValidator::add_issuer` apply SSRF protection by default (use unchecked variants to bypass)
- OAuth redirect URI no longer accepts `0.0.0.0`; only loopback hosts allowed
Security Fixes
- CVE/RUSTSEC-2026-0044, RUSTSEC-2026-0048: TLS cert validation fixed via `aws-lc-sys` update
- CVE/RUSTSEC-2026-0049, RUSTSEC-2026-0098, RUSTSEC-2026-0099: TLS cert validation fixed via `rustls-webpki` update
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
Related context
Beta — feedback welcome: [email protected]