High severityNVD Advisory· Published Apr 24, 2025· Updated Apr 15, 2026
CVE-2025-43855
CVE-2025-43855
Description
tRPC allows users to build & consume fully typesafe APIs without schemas or code generation. In versions starting from 11.0.0 to before 11.1.1, an unhandled error is thrown when validating invalid connectionParams which crashes a tRPC WebSocket server. This allows any unauthenticated user to crash a tRPC 11 WebSocket server. Any tRPC 11 server with WebSocket enabled with a createContext method set is vulnerable. This issue has been patched in version 11.1.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@trpc/servernpm | >= 11.0.0, < 11.1.1 | 11.1.1 |
Patches
29c1c753a778ahttps://github.com/trpc/trpcvia osv
3 files changed · +166 −93
packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts+2 −2 modified@@ -68,8 +68,8 @@ export function fastifyTRPCPlugin<TRouter extends AnyRouter>( ...trpcOptions, }); - fastify.get(prefix ?? '/', { websocket: true }, async (socket, req) => { - await onConnection(socket, req.raw); + fastify.get(prefix ?? '/', { websocket: true }, (socket, req) => { + onConnection(socket, req.raw); if (trpcOptions?.keepAlive?.enabled) { const { pingMs, pongWaitMs } = trpcOptions.keepAlive; handleKeepAlive(socket, pingMs, pongWaitMs);
packages/server/src/adapters/ws.ts+115 −91 modified@@ -34,6 +34,8 @@ import { type MaybePromise, } from '../unstable-core-do-not-import'; // eslint-disable-next-line no-restricted-imports +import type { Result } from '../unstable-core-do-not-import'; +// eslint-disable-next-line no-restricted-imports import { iteratorResource } from '../unstable-core-do-not-import/stream/utils/asyncIterable'; import { Unpromise } from '../vendor/unpromise'; import { createURL, type NodeHTTPCreateContextFnOptions } from './node-http'; @@ -98,14 +100,16 @@ export type WSSHandlerOptions<TRouter extends AnyRouter> = dangerouslyDisablePong?: boolean; }; -const unsetContextPromiseSymbol = Symbol('unsetContextPromise'); export function getWSConnectionHandler<TRouter extends AnyRouter>( opts: WSSHandlerOptions<TRouter>, ) { const { createContext, router } = opts; const { transformer } = router._def._config; - return async (client: ws.WebSocket, req: IncomingMessage) => { + return (client: ws.WebSocket, req: IncomingMessage) => { + type Context = inferRouterContext<TRouter>; + type ContextResult = Result<Context>; + const clientSubscriptions = new Map<number | string, AbortController>(); const abortController = new AbortController(); @@ -122,26 +126,31 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( ); } - function createCtxPromise( + async function createCtxPromise( getConnectionParams: () => TRPCRequestInfo['connectionParams'], - ): Promise<inferRouterContext<TRouter>> { - return run(async () => { - ctx = await createContext?.({ - req, - res: client, - info: { - connectionParams: getConnectionParams(), - calls: [], - isBatchCall: false, - accept: null, - type: 'unknown', - signal: abortController.signal, - url: null, - }, - }); + ): Promise<ContextResult> { + try { + return await run(async (): Promise<ContextResult> => { + ctx = await createContext?.({ + req, + res: client, + info: { + connectionParams: getConnectionParams(), + calls: [], + isBatchCall: false, + accept: null, + type: 'unknown', + signal: abortController.signal, + url: null, + }, + }); - return ctx; - }).catch((cause) => { + return { + ok: true, + value: ctx, + }; + }); + } catch (cause) { const error = getTRPCErrorFromUnknown(cause); opts.onError?.({ error, @@ -167,12 +176,14 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( (globalThis.setImmediate ?? globalThis.setTimeout)(() => { client.close(); }); - - throw error; - }); + return { + ok: false, + error, + }; + } } - let ctx: inferRouterContext<TRouter> | undefined = undefined; + let ctx: Context | undefined = undefined; /** * promise for initializing the context @@ -182,18 +193,40 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( */ let ctxPromise = createURL(req).searchParams.get('connectionParams') === '1' - ? unsetContextPromiseSymbol + ? null : createCtxPromise(() => null); - async function handleRequest(msg: TRPCClientOutgoingMessage) { + function handleRequest(msg: TRPCClientOutgoingMessage) { const { id, jsonrpc } = msg; - /* istanbul ignore next -- @preserve */ if (id === null) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: '`id` is required', + const error = getTRPCErrorFromUnknown( + new TRPCError({ + code: 'PARSE_ERROR', + message: '`id` is required', + }), + ); + opts.onError?.({ + error, + path: undefined, + type: 'unknown', + ctx, + req, + input: undefined, }); + respond({ + id, + jsonrpc, + error: getErrorShape({ + config: router._def._config, + error, + type: 'unknown', + path: undefined, + input: undefined, + ctx, + }), + }); + return; } if (msg.method === 'subscription.stop') { clientSubscriptions.get(id)?.abort(); @@ -202,20 +235,25 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( const { path, lastEventId } = msg.params; let { input } = msg.params; const type = msg.method; - try { - if (lastEventId !== undefined) { - if (isObject(input)) { - input = { - ...input, - lastEventId: lastEventId, - }; - } else { - input ??= { - lastEventId: lastEventId, - }; - } + + if (lastEventId !== undefined) { + if (isObject(input)) { + input = { + ...input, + lastEventId: lastEventId, + }; + } else { + input ??= { + lastEventId: lastEventId, + }; + } + } + run(async () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const res = await ctxPromise!; // asserts context has been set + if (!res.ok) { + throw res.error; } - await ctxPromise; // asserts context has been set const abortController = new AbortController(); const result = await callTRPCProcedure({ @@ -384,7 +422,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( type: 'started', }, }); - } catch (cause) /* istanbul ignore next -- @preserve */ { + }).catch((cause) => { // procedure threw an error const error = getTRPCErrorFromUnknown(cause); opts.onError?.({ error, path, type, ctx, req, input }); @@ -400,9 +438,9 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( ctx, }), }); - } + }); } - client.on('message', async (rawData) => { + client.on('message', (rawData) => { // eslint-disable-next-line @typescript-eslint/no-base-to-string const msgStr = rawData.toString(); if (msgStr === 'PONG') { @@ -414,7 +452,7 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( } return; } - if (ctxPromise === unsetContextPromiseSymbol) { + if (!ctxPromise) { // If the ctxPromise wasn't created immediately, we're expecting the first message to be a TRPCConnectionParamsMessage ctxPromise = createCtxPromise(() => { let msg; @@ -438,31 +476,36 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( }); return; } - try { - const msgJSON: unknown = JSON.parse(msgStr); - const msgs: unknown[] = Array.isArray(msgJSON) ? msgJSON : [msgJSON]; - const promises = msgs - .map((raw) => parseTRPCMessage(raw, transformer)) - .map(handleRequest); - await Promise.all(promises); - } catch (cause) { - const error = new TRPCError({ - code: 'PARSE_ERROR', - cause, - }); - respond({ - id: null, - error: getErrorShape({ - config: router._def._config, - error, - type: 'unknown', - path: undefined, - input: undefined, - ctx: undefined, - }), - }); - } + const parsedMsgs = run(() => { + try { + const msgJSON: unknown = JSON.parse(msgStr); + const msgs: unknown[] = Array.isArray(msgJSON) ? msgJSON : [msgJSON]; + + return msgs.map((raw) => parseTRPCMessage(raw, transformer)); + } catch (cause) { + const error = new TRPCError({ + code: 'PARSE_ERROR', + cause, + }); + + respond({ + id: null, + error: getErrorShape({ + config: router._def._config, + error, + type: 'unknown', + path: undefined, + input: undefined, + ctx, + }), + }); + + return []; + } + }); + + parsedMsgs.map(handleRequest); }); // WebSocket errors should be handled, as otherwise unhandled exceptions will crash Node.js. @@ -487,10 +530,6 @@ export function getWSConnectionHandler<TRouter extends AnyRouter>( clientSubscriptions.clear(); abortController.abort(); }); - - if (ctxPromise !== unsetContextPromiseSymbol) { - await ctxPromise; - } }; } @@ -544,22 +583,7 @@ export function applyWSSHandler<TRouter extends AnyRouter>( return; } - onConnection(client, req).catch((cause) => { - opts.onError?.({ - error: new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - cause, - message: 'Failed to handle WebSocket connection', - }), - req: req, - path: undefined, - type: 'unknown', - ctx: undefined, - input: undefined, - }); - - client.close(); - }); + onConnection(client, req); }); return {
packages/tests/server/websockets.test.ts+49 −0 modified@@ -12,7 +12,9 @@ import { observable, observableToAsyncIterable } from '@trpc/server/observable'; import type { TRPCClientOutgoingMessage, TRPCRequestMessage, + TRPCResponse, } from '@trpc/server/rpc'; +import type { DefaultErrorShape } from '@trpc/server/unstable-core-do-not-import'; import { createDeferred, sleep, @@ -1828,6 +1830,53 @@ describe('auth / connectionParams', async () => { }); expect(ctx.wss.clients.size).toBe(1); }); + + test('regression: bad connection params', async () => { + async function connect() { + const ws = new WebSocket(ctx.wssUrl + '?connectionParams=1'); + await new Promise((resolve) => { + ws.addEventListener('open', resolve); + }); + function request(str: string) { + ws.send(str); + return new Promise<Res>((resolve, reject) => { + ws.addEventListener('message', (it) => { + resolve(JSON.parse(it.data as string)); + }); + ws.addEventListener('error', reject); + ws.addEventListener('close', reject); + }); + } + return { request }; + } + + type Res = TRPCResponse<unknown, DefaultErrorShape>; + + const badConnectionParams = JSON.stringify({ + method: 'connectionParams', + data: { invalidConnectionParams: null }, + }); + + { + const client = await connect(); + const res = await client.request(badConnectionParams); + + assert('error' in res); + expect(res.error.message).toMatchInlineSnapshot( + `"Invalid connection params shape"`, + ); + } + + { + const client = await connect(); + const res = await client.request(badConnectionParams); + + assert('error' in res); + expect(res.error.message).toMatchInlineSnapshot( + `"Invalid connection params shape"`, + ); + } + }); }); describe('subscriptions with createCaller', () => {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-pj3v-9cm8-gvj8ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-43855ghsaADVISORY
- github.com/trpc/trpc/blob/8cef54eaf95d8abc8484fe1d454b6620eeb57f2f/packages/server/src/adapters/ws.tsghsaWEB
- github.com/trpc/trpc/commit/9beb26c636d44852e0f407f3d7a82ad54df65b4dnvdWEB
- github.com/trpc/trpc/pull/5839ghsaWEB
- github.com/trpc/trpc/security/advisories/GHSA-pj3v-9cm8-gvj8nvdWEB
News mentions
0No linked articles in our index yet.