Skip to content

StatelessHTTPServerTransport keys per-request HTTP context by client-chosen JSON-RPC id — concurrent clients with colliding ids can observe each other's HTTPRequest (including Authorization) #265

Description

@vectory-eric

Summary

Server.currentHandlerContext.httpContext is documented as the way for method handlers to observe the originating HTTP request ("headers, auth, path, body"). For StatelessHTTPServerTransport, that context is served from a transport-global dictionary keyed only by the JSON-RPC request id:

  • private var httpRequestContexts: [String: HTTPRequest] = [:]StatelessHTTPServerTransport.swift:58 (v0.12.1)
  • stored on receipt: httpRequestContexts[requestID] = request:224
  • read at dispatch via HTTPContextProviding.httpRequestContext(for:):249, called from Server.swift:785 before the handler runs under Server.$currentHandlerContext.withValue(...)

A stateless transport explicitly serves many independent clients concurrently, and JSON-RPC ids are client-chosen (most clients count from 1). JSON-RPC only requires ids to be unique per client session — per-client uniqueness does not imply transport-global uniqueness, which is exactly what the map assumes. When two in-flight requests share an id, the second store overwrites the first (:224), and either dispatch can read the other client's HTTPRequest in the window between store and dispatch-time read.

Why it matters

The task-local is positioned for auth-adjacent use (the doc comment names "auth" explicitly), so downstream servers resolve per-request authorization from it. In our server (Susurro, a local-first macOS app), handlers map the Authorization: Bearer token to a permission scope via currentHandlerContext.httpContext — under an id collision, a lower-privileged client's handler can (probabilistically) observe a concurrent higher-privileged client's Authorization header and act with its scope. Severity is deployment-dependent (ours is loopback-only with pre-authenticated clients), but for any server using httpContext in authorization decisions this is a cross-client confused-deputy risk, not just a correctness nit.

The response-waiter plumbing appears to share the same root cause (id-keyed transport-global state), so colliding ids can presumably also clobber each other's response continuations — worth fixing together.

Reproduction sketch

  1. Start a Server on StatelessHTTPServerTransport with a CallTool handler that echoes Server.currentHandlerContext?.httpContext?.header("X-Marker") back in its result.
  2. Run two concurrent client loops POSTing tools/call with the same "id": 1, each sending a distinct X-Marker header.
  3. Assert every response's echoed marker matches its own request. Under load, mismatches appear — each one is a swapped context.

Suggested fix direction

handlePOST has the per-request HTTPRequest in hand before it enqueues the message; binding it through the dispatch path (paired with the message through the pump, or keyed by a transport-generated unique per-request token instead of the client-chosen JSON-RPC id) would make HandlerContext.httpContext genuinely per-dispatch. Alternatively/additionally, rejecting duplicate in-flight ids would close the collision at the door — though that changes observable behavior for clients that legitimately reuse ids across their own sequential requests.

Happy to provide more detail or test our workload against a patch. Thanks for the SDK — the task-local context design is otherwise exactly what a server like ours needs.


Version: swift-sdk 0.12.1 (exact pin). Server stack: StatelessHTTPServerTransport + FlyingFox HTTP listener bridging into transport.handleRequest, custom validation pipeline, Swift 6.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions