mirror of
https://github.com/nestjs/nest.git
synced 2026-10-29 02:06:11 +08:00
fix(microservices): handle unserializable patterns without crashing
"JSON.stringify" throws a RangeError for deeply nested message patterns. The TCP and RMQ servers stringified client-controlled patterns without guarding against it, and the resulting unhandled promise rejection terminated the process. Patterns are now stringified safely (falling back to a sentinel that matches no handler) and rejected message handling is routed to "handleError" instead of being left unhandled. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
dda252090e
commit
aa97b5144d
6 files changed
+117
-15
No files matched your search
@@ -63,3 +63,4 @@ export const BLOCKED_RMQ_MESSAGE = (reason: string) =>
|
||||
export const UNBLOCKED_RMQ_MESSAGE = 'RMQ broker has unblocked the connection.';
|
||||
|
||||
export const NATS_DEFAULT_GRACE_PERIOD = 10000;
|
||||
export const UNSERIALIZABLE_PATTERN = '[UNSERIALIZABLE_PATTERN]';
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
} from '../interfaces/packet.interface.js';
|
||||
import { RmqRecordSerializer } from '../serializers/rmq-record.serializer.js';
|
||||
import { Server } from './server.js';
|
||||
import { isNil, isString, isUndefined } from '@nestjs/common/internal';
|
||||
import { isNil, isUndefined } from '@nestjs/common/internal';
|
||||
|
||||
// To enable type safety for RMQ. This cant be uncommented by default
|
||||
// because it would require the user to install the amqplib package even if they dont use RabbitMQ
|
||||
@@ -283,7 +283,8 @@ export class ServerRMQ extends Server<RmqEvents, RmqStatus> {
|
||||
await channel.prefetch(prefetchCount, isGlobalPrefetchCount);
|
||||
channel.consume(
|
||||
createdQueue,
|
||||
(msg: Record<string, any> | null) => this.handleMessage(msg!, channel),
|
||||
(msg: Record<string, any> | null) =>
|
||||
this.handleMessage(msg!, channel).catch(err => this.handleError(err)),
|
||||
{
|
||||
noAck: this.noAck,
|
||||
consumerTag: this.getOptionsProp(
|
||||
@@ -306,9 +307,7 @@ export class ServerRMQ extends Server<RmqEvents, RmqStatus> {
|
||||
const { content, properties } = message;
|
||||
const rawMessage = this.parseMessageContent(content);
|
||||
const packet = await this.deserializer.deserialize(rawMessage, properties);
|
||||
const pattern = isString(packet.pattern)
|
||||
? packet.pattern
|
||||
: JSON.stringify(packet.pattern);
|
||||
const pattern = this.getPatternAsString(packet.pattern);
|
||||
|
||||
const rmqContext = new RmqContext([message, channel, pattern]);
|
||||
if (isUndefined((packet as IncomingRequest).id)) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
TransportId,
|
||||
} from '../interfaces/microservice-configuration.interface.js';
|
||||
import { Server } from './server.js';
|
||||
import { isString, isUndefined } from '@nestjs/common/internal';
|
||||
import { isUndefined } from '@nestjs/common/internal';
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
@@ -81,8 +81,8 @@ export class ServerTCP extends Server<TcpEvents, TcpStatus> {
|
||||
|
||||
public bindHandler(socket: Socket) {
|
||||
const readSocket = this.getSocketInstance(socket);
|
||||
readSocket.on('message', async (msg: ReadPacket & PacketId) =>
|
||||
this.handleMessage(readSocket, msg),
|
||||
readSocket.on('message', (msg: ReadPacket & PacketId) =>
|
||||
this.handleMessage(readSocket, msg).catch(err => this.handleError(err)),
|
||||
);
|
||||
readSocket.on(TcpEventsMap.ERROR, err => {
|
||||
const invalidError = new InvalidTcpDataReceptionException(err);
|
||||
@@ -92,9 +92,7 @@ export class ServerTCP extends Server<TcpEvents, TcpStatus> {
|
||||
|
||||
public async handleMessage(socket: TcpSocket, rawMessage: unknown) {
|
||||
const packet = await this.deserializer.deserialize(rawMessage);
|
||||
const pattern = !isString(packet.pattern)
|
||||
? JSON.stringify(packet.pattern)
|
||||
: packet.pattern;
|
||||
const pattern = this.getPatternAsString(packet.pattern);
|
||||
|
||||
const tcpContext = new TcpContext([socket, pattern]);
|
||||
if (isUndefined((packet as IncomingRequest).id)) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
finalize,
|
||||
mergeMap,
|
||||
} from 'rxjs/operators';
|
||||
import { NO_EVENT_HANDLER } from '../constants.js';
|
||||
import { NO_EVENT_HANDLER, UNSERIALIZABLE_PATTERN } from '../constants.js';
|
||||
import { BaseRpcContext } from '../ctx-host/base-rpc.context.js';
|
||||
import { IncomingRequestDeserializer } from '../deserializers/incoming-request.deserializer.js';
|
||||
import { Transport } from '../enums/index.js';
|
||||
@@ -39,7 +39,11 @@ import { ConsumerSerializer } from '../interfaces/serializer.interface.js';
|
||||
import { IdentitySerializer } from '../serializers/identity.serializer.js';
|
||||
import { transformPatternToRoute } from '../utils/index.js';
|
||||
import { ITransportServer, Logger, type LoggerService } from '@nestjs/common';
|
||||
import { loadPackage, loadPackageSync } from '@nestjs/common/internal';
|
||||
import {
|
||||
isString,
|
||||
loadPackage,
|
||||
loadPackageSync,
|
||||
} from '@nestjs/common/internal';
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
@@ -356,4 +360,25 @@ export abstract class Server<
|
||||
protected normalizePattern(pattern: MsPattern): string {
|
||||
return transformPatternToRoute(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of an incoming message pattern.
|
||||
*
|
||||
* Patterns are client-controlled: serializing a deeply nested one makes
|
||||
* `JSON.stringify` throw a `RangeError`, which must not escape the message
|
||||
* handler (as an unhandled promise rejection that terminates the process).
|
||||
*
|
||||
* @param {unknown} pattern - client pattern
|
||||
* @returns string
|
||||
*/
|
||||
protected getPatternAsString(pattern: unknown): string {
|
||||
if (isString(pattern)) {
|
||||
return pattern;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(pattern);
|
||||
} catch {
|
||||
return UNSERIALIZABLE_PATTERN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,12 +107,16 @@ describe('ServerRMQ', () => {
|
||||
});
|
||||
|
||||
describe('handleMessage', () => {
|
||||
const createMessage = payload => ({
|
||||
const createRawMessage = (content: string) => ({
|
||||
content: {
|
||||
toString: () => JSON.stringify(payload),
|
||||
toString: () => content,
|
||||
},
|
||||
properties: { correlationId: 1 },
|
||||
});
|
||||
const createMessage = payload => createRawMessage(JSON.stringify(payload));
|
||||
// "JSON.parse" copes with this depth but "JSON.stringify" throws a RangeError
|
||||
const createDeeplyNestedJson = (depth: number) =>
|
||||
'{"nested":'.repeat(depth) + '{}' + '}'.repeat(depth);
|
||||
const pattern = 'test';
|
||||
const msg = createMessage({
|
||||
pattern,
|
||||
@@ -139,6 +143,22 @@ describe('ServerRMQ', () => {
|
||||
await server.handleMessage(createMessage({ pattern: '', data: '' }), '');
|
||||
expect(handleEventSpy).toHaveBeenCalled();
|
||||
});
|
||||
it('should send NO_MESSAGE_HANDLER error if pattern is too deeply nested to be serialized', async () => {
|
||||
const deeplyNestedMsg = createRawMessage(
|
||||
`{"pattern":${createDeeplyNestedJson(100_000)},"data":"tests","id":"3"}`,
|
||||
);
|
||||
await server.handleMessage(deeplyNestedMsg, '');
|
||||
expect(sendMessageStub).toHaveBeenCalledWith(
|
||||
{
|
||||
id: '3',
|
||||
err: NO_MESSAGE_HANDLER,
|
||||
status: 'error',
|
||||
},
|
||||
undefined,
|
||||
1,
|
||||
expect.any(RmqContext),
|
||||
);
|
||||
});
|
||||
it('should send NO_MESSAGE_HANDLER error if key does not exists in handlers object', async () => {
|
||||
await server.handleMessage(msg, '');
|
||||
expect(sendMessageStub).toHaveBeenCalledWith(
|
||||
@@ -278,6 +298,19 @@ describe('ServerRMQ', () => {
|
||||
await server.setupChannel(channel, () => null);
|
||||
expect(channel.consume).toHaveBeenCalled();
|
||||
});
|
||||
it('should route "handleMessage" rejections to "handleError" instead of leaving them unhandled', async () => {
|
||||
const error = new Error('unexpected');
|
||||
vi.spyOn(server, 'handleMessage').mockRejectedValue(error);
|
||||
const handleErrorSpy = vi
|
||||
.spyOn(untypedServer, 'handleError')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
await server.setupChannel(channel, () => null);
|
||||
const [, onMessage] = channel.consume.mock.calls[0];
|
||||
await onMessage({});
|
||||
|
||||
expect(handleErrorSpy).toHaveBeenCalledWith(error);
|
||||
});
|
||||
it('should call "resolve" function', async () => {
|
||||
const resolve = vi.fn();
|
||||
await server.setupChannel(channel, resolve);
|
||||
|
||||
@@ -5,6 +5,14 @@ import { TcpSocket } from '../../helpers/tcp-socket.js';
|
||||
import { ServerTCP } from '../../server/server-tcp.js';
|
||||
import { objectToMap } from './utils/object-to-map.js';
|
||||
|
||||
const createDeeplyNestedObject = (depth: number) => {
|
||||
let pattern: Record<string, unknown> = {};
|
||||
for (let i = 0; i < depth; i++) {
|
||||
pattern = { nested: pattern };
|
||||
}
|
||||
return pattern;
|
||||
};
|
||||
|
||||
describe('ServerTCP', () => {
|
||||
let server: ServerTCP;
|
||||
let untypedServer: any;
|
||||
@@ -26,6 +34,21 @@ describe('ServerTCP', () => {
|
||||
server.bindHandler(null!);
|
||||
expect(socket.on).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('should route "handleMessage" rejections to "handleError" instead of leaving them unhandled', async () => {
|
||||
const error = new Error('unexpected');
|
||||
vi.spyOn(server, 'handleMessage').mockRejectedValue(error);
|
||||
const handleErrorSpy = vi
|
||||
.spyOn(untypedServer, 'handleError')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
server.bindHandler(null!);
|
||||
const [, onMessage] = socket.on.mock.calls.find(
|
||||
([event]) => event === 'message',
|
||||
);
|
||||
await onMessage({});
|
||||
|
||||
expect(handleErrorSpy).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
describe('close', () => {
|
||||
const tcpServer = { close: vi.fn() };
|
||||
@@ -80,6 +103,29 @@ describe('ServerTCP', () => {
|
||||
await server.handleMessage(socket, msg);
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
it('should send NO_MESSAGE_HANDLER error if pattern is too deeply nested to be serialized', async () => {
|
||||
const deeplyNestedMsg = {
|
||||
...msg,
|
||||
pattern: createDeeplyNestedObject(100_000),
|
||||
};
|
||||
await server.handleMessage(socket, deeplyNestedMsg);
|
||||
expect(socket.sendMessage).toHaveBeenCalledWith({
|
||||
id: msg.id,
|
||||
status: 'error',
|
||||
err: NO_MESSAGE_HANDLER,
|
||||
});
|
||||
});
|
||||
it('should call "handleEvent" if pattern is too deeply nested to be serialized and identifier is not present', async () => {
|
||||
const handleEventSpy = vi
|
||||
.spyOn(server, 'handleEvent')
|
||||
.mockImplementation(async () => undefined);
|
||||
const deeplyNestedEvent = {
|
||||
pattern: createDeeplyNestedObject(100_000),
|
||||
data: 'tests',
|
||||
};
|
||||
await server.handleMessage(socket, deeplyNestedEvent);
|
||||
expect(handleEventSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
describe('handleClose', () => {
|
||||
describe('when is terminated', () => {
|
||||
|
||||
Reference in new issue
Block a user