mirror of
https://github.com/nestjs/nest.git
synced 2026-10-28 09:45:43 +08:00
fix(express,fastify): limit field nesting and array indexes by default
fieldNestingDepth and fieldArrayIndexLimit (#18074) defaulted to Infinity, so a single request could build an arbitrarily deep req.body or arrays of arbitrary length. They now default to 32 and 1000 on both adapters; set them to Infinity to lift the limits. Discovered by Anthropic's OSS Scanner, as vulnerabilities ANT-2026-LDR3CR1E and ANT-2026-SBX67YG6.
This commit is contained in:
20 files changed
+352
-47
No files matched your search
@@ -476,6 +476,29 @@ describe('File upload: Express (multer) and Fastify parity', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should limit field name nesting and array indexes by default', async () => {
|
||||
const ok = await expectParity(http =>
|
||||
http
|
||||
.post('/none')
|
||||
.field('a' + '[b]'.repeat(32), 'x')
|
||||
.field('c[1000]', 'y'),
|
||||
);
|
||||
expect(ok.status).toBe(201);
|
||||
for (const [name, message] of [
|
||||
['a' + '[b]'.repeat(33), 'Field name nesting too deep'],
|
||||
['c[1001]', 'Field name array index too large'],
|
||||
]) {
|
||||
const res = await expectParity(http =>
|
||||
http.post('/none').field(name, 'x'),
|
||||
);
|
||||
expect(res.body).toEqual({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: `${message} - ${name}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should not apply fieldArrayIndexLimit to a name kept as a literal key', async () => {
|
||||
const res = await expectParity(http =>
|
||||
http.post('/name-limits').field('a[3]x', '1').field('[3]', '2'),
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
|
||||
import { MulterModuleOptions } from '../interfaces/index.js';
|
||||
import { MulterOptions } from '../interfaces/multer-options.interface.js';
|
||||
import {
|
||||
applyDefaultLimits,
|
||||
mergeMulterOptions,
|
||||
transformException,
|
||||
} from '../multer/multer.utils.js';
|
||||
@@ -35,7 +36,9 @@ export function AnyFilesInterceptor(
|
||||
@Inject(MULTER_MODULE_OPTIONS)
|
||||
options: MulterModuleOptions = {},
|
||||
) {
|
||||
this.multer = (multer as any)(mergeMulterOptions(options, localOptions));
|
||||
this.multer = (multer as any)(
|
||||
applyDefaultLimits(mergeMulterOptions(options, localOptions)),
|
||||
);
|
||||
}
|
||||
|
||||
async intercept(
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
MulterOptions,
|
||||
} from '../interfaces/multer-options.interface.js';
|
||||
import {
|
||||
applyDefaultLimits,
|
||||
mergeMulterOptions,
|
||||
transformException,
|
||||
} from '../multer/multer.utils.js';
|
||||
@@ -39,7 +40,9 @@ export function FileFieldsInterceptor(
|
||||
@Inject(MULTER_MODULE_OPTIONS)
|
||||
options: MulterModuleOptions = {},
|
||||
) {
|
||||
this.multer = (multer as any)(mergeMulterOptions(options, localOptions));
|
||||
this.multer = (multer as any)(
|
||||
applyDefaultLimits(mergeMulterOptions(options, localOptions)),
|
||||
);
|
||||
}
|
||||
|
||||
async intercept(
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
|
||||
import { MulterModuleOptions } from '../interfaces/index.js';
|
||||
import { MulterOptions } from '../interfaces/multer-options.interface.js';
|
||||
import {
|
||||
applyDefaultLimits,
|
||||
mergeMulterOptions,
|
||||
transformException,
|
||||
} from '../multer/multer.utils.js';
|
||||
@@ -37,7 +38,9 @@ export function FileInterceptor(
|
||||
@Inject(MULTER_MODULE_OPTIONS)
|
||||
options: MulterModuleOptions = {},
|
||||
) {
|
||||
this.multer = (multer as any)(mergeMulterOptions(options, localOptions));
|
||||
this.multer = (multer as any)(
|
||||
applyDefaultLimits(mergeMulterOptions(options, localOptions)),
|
||||
);
|
||||
}
|
||||
|
||||
async intercept(
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
|
||||
import { MulterModuleOptions } from '../interfaces/index.js';
|
||||
import { MulterOptions } from '../interfaces/multer-options.interface.js';
|
||||
import {
|
||||
applyDefaultLimits,
|
||||
mergeMulterOptions,
|
||||
transformException,
|
||||
} from '../multer/multer.utils.js';
|
||||
@@ -40,7 +41,9 @@ export function FilesInterceptor(
|
||||
@Inject(MULTER_MODULE_OPTIONS)
|
||||
options: MulterModuleOptions = {},
|
||||
) {
|
||||
this.multer = (multer as any)(mergeMulterOptions(options, localOptions));
|
||||
this.multer = (multer as any)(
|
||||
applyDefaultLimits(mergeMulterOptions(options, localOptions)),
|
||||
);
|
||||
}
|
||||
|
||||
async intercept(
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MULTER_MODULE_OPTIONS } from '../files.constants.js';
|
||||
import { MulterModuleOptions } from '../interfaces/index.js';
|
||||
import { MulterOptions } from '../interfaces/multer-options.interface.js';
|
||||
import {
|
||||
applyDefaultLimits,
|
||||
mergeMulterOptions,
|
||||
transformException,
|
||||
} from '../multer/multer.utils.js';
|
||||
@@ -35,7 +36,9 @@ export function NoFilesInterceptor(
|
||||
@Inject(MULTER_MODULE_OPTIONS)
|
||||
options: MulterModuleOptions = {},
|
||||
) {
|
||||
this.multer = (multer as any)(mergeMulterOptions(options, localOptions));
|
||||
this.multer = (multer as any)(
|
||||
applyDefaultLimits(mergeMulterOptions(options, localOptions)),
|
||||
);
|
||||
}
|
||||
|
||||
async intercept(
|
||||
|
||||
@@ -16,9 +16,17 @@ export interface MulterLimits {
|
||||
parts?: number;
|
||||
/** For multipart forms, the max number of header key=> value pairs to parse Default: 2000(same as node's http). */
|
||||
headerPairs?: number;
|
||||
/** Max nesting depth of field names; `a[b][c]` has 2 levels (Default: Infinity) */
|
||||
/**
|
||||
* Max nesting depth of field names; `a[b][c]` has 2 levels (Default: 32,
|
||||
* where multer's own default is Infinity). Set it to `Infinity` to lift
|
||||
* the limit.
|
||||
*/
|
||||
fieldNestingDepth?: number;
|
||||
/** Max numeric array index accepted in field names; `a[3]` uses index 3 (Default: Infinity) */
|
||||
/**
|
||||
* Max numeric array index accepted in field names; `a[3]` uses index 3
|
||||
* (Default: 1000, where multer's own default is Infinity). Set it to
|
||||
* `Infinity` to lift the limit.
|
||||
*/
|
||||
fieldArrayIndexLimit?: number;
|
||||
}
|
||||
|
||||
@@ -32,8 +40,9 @@ export interface MulterOptions {
|
||||
/** The storage engine to use for uploaded files. */
|
||||
storage?: any;
|
||||
/**
|
||||
* An object specifying the size limits of the following optional properties. This object is passed to busboy
|
||||
* directly, and the details of properties can be found on https://github.com/mscdex/busboy#busboy-methods
|
||||
* An object specifying the size limits of the following optional properties. This object is passed to multer
|
||||
* and on to busboy, with `fieldNestingDepth` and `fieldArrayIndexLimit` set to their defaults when absent.
|
||||
* The details of the busboy properties can be found on https://github.com/mscdex/busboy#busboy-methods
|
||||
*
|
||||
* Since multer 2.4.0, this can also be a function of the request that returns such an object.
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,18 @@ export const multerExceptions = {
|
||||
// stays a 500.
|
||||
};
|
||||
|
||||
/**
|
||||
* Defaults of the field name limits that multer leaves at `Infinity`, which
|
||||
* let a single small request build an arbitrarily deep `req.body`
|
||||
* (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
|
||||
* `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
|
||||
* ExpressAdapter parses; `1000` is the number of parameters it parses.
|
||||
*/
|
||||
export const DEFAULT_FIELD_NAME_LIMITS = {
|
||||
fieldNestingDepth: 32,
|
||||
fieldArrayIndexLimit: 1000,
|
||||
} as const;
|
||||
|
||||
export const busboyExceptions = {
|
||||
// from https://github.com/mscdex/busboy/blob/master/lib/types/multipart.js
|
||||
MULTIPART_BOUNDARY_NOT_FOUND: 'Multipart: Boundary not found',
|
||||
|
||||
@@ -4,9 +4,16 @@ import {
|
||||
PayloadTooLargeException,
|
||||
} from '@nestjs/common';
|
||||
import { isPlainObject } from '@nestjs/common/internal';
|
||||
import { multerExceptions, busboyExceptions } from './multer.constants.js';
|
||||
import {
|
||||
DEFAULT_FIELD_NAME_LIMITS,
|
||||
multerExceptions,
|
||||
busboyExceptions,
|
||||
} from './multer.constants.js';
|
||||
import { MulterModuleOptions } from '../interfaces/index.js';
|
||||
import { MulterOptions } from '../interfaces/multer-options.interface.js';
|
||||
import {
|
||||
MulterLimits,
|
||||
MulterOptions,
|
||||
} from '../interfaces/multer-options.interface.js';
|
||||
|
||||
/**
|
||||
* Merges module-level (global) multer options with interceptor-level (local)
|
||||
@@ -29,6 +36,36 @@ export function mergeMulterOptions(
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field name limits multer leaves at `Infinity` to
|
||||
* `DEFAULT_FIELD_NAME_LIMITS` wherever they are not set. multer only checks
|
||||
* a limit that is present in `limits`, so the defaults are added to the
|
||||
* object, or to the object a function-valued `limits` returns.
|
||||
*/
|
||||
export function applyDefaultLimits<T extends MulterOptions>(options: T): T {
|
||||
const { limits } = options;
|
||||
return {
|
||||
...options,
|
||||
limits:
|
||||
typeof limits === 'function'
|
||||
? (req: any) => withDefaultLimits(limits(req))
|
||||
: withDefaultLimits(limits),
|
||||
};
|
||||
}
|
||||
|
||||
function withDefaultLimits(
|
||||
limits: MulterLimits | null | undefined,
|
||||
): MulterLimits {
|
||||
return {
|
||||
...limits,
|
||||
fieldNestingDepth:
|
||||
limits?.fieldNestingDepth ?? DEFAULT_FIELD_NAME_LIMITS.fieldNestingDepth,
|
||||
fieldArrayIndexLimit:
|
||||
limits?.fieldArrayIndexLimit ??
|
||||
DEFAULT_FIELD_NAME_LIMITS.fieldArrayIndexLimit,
|
||||
};
|
||||
}
|
||||
|
||||
// Multer may add in a 'field' property to the error
|
||||
// https://github.com/expressjs/multer/blob/aa42bea6ac7d0cb8fcb279b15a7278cda805dc63/lib/multer-error.js#L19
|
||||
export function transformException(
|
||||
|
||||
@@ -40,10 +40,15 @@ describe('FilesInterceptor', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should merge global and local limits key-by-key', () => {
|
||||
it('should merge global and local limits key-by-key, over the default field name limits', () => {
|
||||
const target = new (AnyFilesInterceptor({ limits: { fileSize: 10 } }))({
|
||||
limits: { files: 2 },
|
||||
});
|
||||
expect((target as any).multer.limits).toEqual({ files: 2, fileSize: 10 });
|
||||
expect((target as any).multer.limits).toEqual({
|
||||
files: 2,
|
||||
fileSize: 10,
|
||||
fieldNestingDepth: 32,
|
||||
fieldArrayIndexLimit: 1000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -59,10 +59,15 @@ describe('FileFieldsInterceptor', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should merge global and local limits key-by-key', () => {
|
||||
it('should merge global and local limits key-by-key, over the default field name limits', () => {
|
||||
const target = new (FileFieldsInterceptor([{ name: 'file', maxCount: 1 }], {
|
||||
limits: { fileSize: 10 },
|
||||
}))({ limits: { files: 2 } });
|
||||
expect((target as any).multer.limits).toEqual({ files: 2, fileSize: 10 });
|
||||
expect((target as any).multer.limits).toEqual({
|
||||
files: 2,
|
||||
fileSize: 10,
|
||||
fieldNestingDepth: 32,
|
||||
fieldArrayIndexLimit: 1000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,10 +41,15 @@ describe('FileInterceptor', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should merge global and local limits key-by-key', () => {
|
||||
it('should merge global and local limits key-by-key, over the default field name limits', () => {
|
||||
const target = new (FileInterceptor('file', {
|
||||
limits: { fileSize: 10 },
|
||||
}))({ limits: { files: 2 } });
|
||||
expect((target as any).multer.limits).toEqual({ files: 2, fileSize: 10 });
|
||||
expect((target as any).multer.limits).toEqual({
|
||||
files: 2,
|
||||
fileSize: 10,
|
||||
fieldNestingDepth: 32,
|
||||
fieldArrayIndexLimit: 1000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -43,10 +43,15 @@ describe('FilesInterceptor', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should merge global and local limits key-by-key', () => {
|
||||
it('should merge global and local limits key-by-key, over the default field name limits', () => {
|
||||
const target = new (FilesInterceptor('file', 10, {
|
||||
limits: { fileSize: 10 },
|
||||
}))({ limits: { files: 2 } });
|
||||
expect((target as any).multer.limits).toEqual({ files: 2, fileSize: 10 });
|
||||
expect((target as any).multer.limits).toEqual({
|
||||
files: 2,
|
||||
fileSize: 10,
|
||||
fieldNestingDepth: 32,
|
||||
fieldArrayIndexLimit: 1000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -39,10 +39,15 @@ describe('NoFilesInterceptor', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should merge global and local limits key-by-key', () => {
|
||||
it('should merge global and local limits key-by-key, over the default field name limits', () => {
|
||||
const target = new (NoFilesInterceptor({ limits: { fileSize: 10 } }))({
|
||||
limits: { files: 2 },
|
||||
});
|
||||
expect((target as any).multer.limits).toEqual({ files: 2, fileSize: 10 });
|
||||
expect((target as any).multer.limits).toEqual({
|
||||
files: 2,
|
||||
fileSize: 10,
|
||||
fieldNestingDepth: 32,
|
||||
fieldArrayIndexLimit: 1000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,9 +9,12 @@ import {
|
||||
multerExceptions,
|
||||
} from '../../../multer/multer/multer.constants.js';
|
||||
import {
|
||||
applyDefaultLimits,
|
||||
mergeMulterOptions,
|
||||
transformException,
|
||||
} from '../../../multer/multer/multer.utils.js';
|
||||
import { MulterOptions } from '../../../multer/interfaces/multer-options.interface.js';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
describe('mergeMulterOptions', () => {
|
||||
it('should let local options override top-level global options', () => {
|
||||
@@ -62,6 +65,125 @@ describe('mergeMulterOptions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyDefaultLimits', () => {
|
||||
const defaults = { fieldNestingDepth: 32, fieldArrayIndexLimit: 1000 };
|
||||
|
||||
it('should set the field name limits when no limits are set', () => {
|
||||
const options = applyDefaultLimits({ dest: '/uploads' });
|
||||
expect(options).toEqual({ dest: '/uploads', limits: defaults });
|
||||
});
|
||||
|
||||
it('should keep the other limits and the field name limits that are set', () => {
|
||||
expect(
|
||||
applyDefaultLimits({
|
||||
limits: { fileSize: 10, fieldNestingDepth: 0 },
|
||||
}).limits,
|
||||
).toEqual({
|
||||
fileSize: 10,
|
||||
fieldNestingDepth: 0,
|
||||
fieldArrayIndexLimit: 1000,
|
||||
});
|
||||
expect(
|
||||
applyDefaultLimits({
|
||||
limits: { fieldNestingDepth: Infinity, fieldArrayIndexLimit: Infinity },
|
||||
}).limits,
|
||||
).toEqual({ fieldNestingDepth: Infinity, fieldArrayIndexLimit: Infinity });
|
||||
});
|
||||
|
||||
it('should set the field name limits that are null or undefined', () => {
|
||||
expect(
|
||||
applyDefaultLimits({
|
||||
limits: { fieldNestingDepth: undefined, fieldArrayIndexLimit: null! },
|
||||
}).limits,
|
||||
).toEqual(defaults);
|
||||
});
|
||||
|
||||
it('should not modify the options it is given', () => {
|
||||
const options = { limits: { fileSize: 10 } };
|
||||
applyDefaultLimits(options);
|
||||
expect(options).toEqual({ limits: { fileSize: 10 } });
|
||||
});
|
||||
|
||||
it('should set the field name limits on the limits a function returns', () => {
|
||||
const { limits } = applyDefaultLimits({
|
||||
limits: (req: { size: number }) => ({ fileSize: req.size }),
|
||||
});
|
||||
expect((limits as Function)({ size: 10 })).toEqual({
|
||||
fileSize: 10,
|
||||
...defaults,
|
||||
});
|
||||
expect(
|
||||
(applyDefaultLimits({ limits: () => undefined! }).limits as Function)({}),
|
||||
).toEqual(defaults);
|
||||
});
|
||||
|
||||
describe('with multer', () => {
|
||||
/** Runs `multer(options).none()` over a body with one text field. */
|
||||
const parseField = (options: MulterOptions, fieldName: string) => {
|
||||
const boundary = 'nestjs';
|
||||
const body =
|
||||
`--${boundary}\r\n` +
|
||||
`Content-Disposition: form-data; name="${fieldName}"\r\n\r\n` +
|
||||
`x\r\n` +
|
||||
`--${boundary}--\r\n`;
|
||||
const req = Object.assign(Readable.from([Buffer.from(body)]), {
|
||||
headers: {
|
||||
'content-type': `multipart/form-data; boundary=${boundary}`,
|
||||
'content-length': String(Buffer.byteLength(body)),
|
||||
},
|
||||
});
|
||||
return new Promise<any>((resolve, reject) =>
|
||||
multer(applyDefaultLimits(options)).none()(
|
||||
req as any,
|
||||
{} as any,
|
||||
(err?: any) => (err ? reject(err) : resolve((req as any).body)),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
/** Reads `a.b.b...` (`depth` times `b`) out of a parsed body. */
|
||||
const dig = (body: any, depth: number) => {
|
||||
let value = body.a;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
value = value.b;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
it('should reject names nested deeper than 32 levels by default', async () => {
|
||||
expect(dig(await parseField({}, 'a' + '[b]'.repeat(32)), 32)).toBe('x');
|
||||
await expect(
|
||||
parseField({}, 'a' + '[b]'.repeat(33)),
|
||||
).rejects.toMatchObject({ code: 'LIMIT_FIELD_NESTING' });
|
||||
});
|
||||
|
||||
it('should reject array indexes over 1000 by default', async () => {
|
||||
expect((await parseField({}, 'a[1000]')).a).toHaveLength(1001);
|
||||
await expect(parseField({}, 'a[1001]')).rejects.toMatchObject({
|
||||
code: 'LIMIT_FIELD_ARRAY_INDEX',
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply the defaults to the limits a function returns', async () => {
|
||||
await expect(
|
||||
parseField({ limits: () => ({}) }, 'a[1001]'),
|
||||
).rejects.toMatchObject({ code: 'LIMIT_FIELD_ARRAY_INDEX' });
|
||||
});
|
||||
|
||||
it('should lift the defaults when set to Infinity', async () => {
|
||||
const limits = {
|
||||
fieldNestingDepth: Infinity,
|
||||
fieldArrayIndexLimit: Infinity,
|
||||
};
|
||||
const body = await parseField(
|
||||
{ limits },
|
||||
'a' + '[b]'.repeat(33) + '[1001]',
|
||||
);
|
||||
expect(dig(body, 33)).toHaveLength(1002);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformException', () => {
|
||||
describe('if error does not exist', () => {
|
||||
it('should behave as identity', () => {
|
||||
|
||||
@@ -53,15 +53,17 @@ export interface MultipartLimits {
|
||||
/** For multipart forms, the max number of header key => value pairs to parse (Default: 2000) */
|
||||
headerPairs?: number;
|
||||
/**
|
||||
* Max nesting depth of field names; `a[b][c]` has 2 levels (Default:
|
||||
* Infinity). Like `fieldNameSize`, it does not fall back to the options
|
||||
* Max nesting depth of field names; `a[b][c]` has 2 levels (Default: 32,
|
||||
* where multer's own default is Infinity). Set it to `Infinity` to lift
|
||||
* the limit. Like `fieldNameSize`, it does not fall back to the options
|
||||
* `@fastify/multipart` was registered with.
|
||||
*/
|
||||
fieldNestingDepth?: number;
|
||||
/**
|
||||
* Max numeric array index accepted in field names; `a[3]` uses index 3
|
||||
* (Default: Infinity). Like `fieldNameSize`, it does not fall back to the
|
||||
* options `@fastify/multipart` was registered with.
|
||||
* (Default: 1000, where multer's own default is Infinity). Set it to
|
||||
* `Infinity` to lift the limit. Like `fieldNameSize`, it does not fall back
|
||||
* to the options `@fastify/multipart` was registered with.
|
||||
*/
|
||||
fieldArrayIndexLimit?: number;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,20 @@ export const multerExceptions = {
|
||||
|
||||
export type MultipartErrorCode = keyof typeof multerExceptions;
|
||||
|
||||
/**
|
||||
* Defaults of the field name limits that multer leaves at `Infinity`, which
|
||||
* let a single small request build an arbitrarily deep `req.body`
|
||||
* (`fieldNestingDepth`) or arrays of arbitrary length (`fieldArrayIndexLimit`).
|
||||
* `32` is the nesting depth `body-parser` allows in the urlencoded bodies the
|
||||
* ExpressAdapter parses; `1000` is the number of parameters it parses, and
|
||||
* the number of parts `@fastify/multipart` accepts. Kept identical to
|
||||
* platform-express's `DEFAULT_FIELD_NAME_LIMITS`.
|
||||
*/
|
||||
export const DEFAULT_FIELD_NAME_LIMITS = {
|
||||
fieldNestingDepth: 32,
|
||||
fieldArrayIndexLimit: 1000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* The messages platform-express reports for busboy 1.x parser errors
|
||||
* (prefixed with "Multipart: "), which is what multer bundles.
|
||||
|
||||
@@ -16,6 +16,7 @@ import { memoryStorage } from '../storage/memory.storage.js';
|
||||
import { appendField } from './append-field.util.js';
|
||||
import {
|
||||
busboyExceptions,
|
||||
DEFAULT_FIELD_NAME_LIMITS,
|
||||
MISSING_PLUGIN_MESSAGE,
|
||||
multerExceptions,
|
||||
} from './multipart.constants.js';
|
||||
@@ -333,20 +334,23 @@ function exceedsArrayIndexLimit(name: string, limit: number) {
|
||||
/**
|
||||
* `fieldNestingDepth` and `fieldArrayIndexLimit` are multer's own limits
|
||||
* (busboy knows neither), so they are checked here, against the decoded name.
|
||||
* Unlike multer, they default to `DEFAULT_FIELD_NAME_LIMITS`.
|
||||
*/
|
||||
function checkFieldNameShape(
|
||||
name: string,
|
||||
limits: MultipartLimits | undefined,
|
||||
) {
|
||||
if (
|
||||
typeof limits?.fieldNestingDepth === 'number' &&
|
||||
name.split('[').length - 1 > limits.fieldNestingDepth
|
||||
) {
|
||||
const nestingDepth =
|
||||
limits?.fieldNestingDepth ?? DEFAULT_FIELD_NAME_LIMITS.fieldNestingDepth;
|
||||
if (name.split('[').length - 1 > nestingDepth) {
|
||||
throw new MultipartError('LIMIT_FIELD_NESTING', name);
|
||||
}
|
||||
const arrayIndexLimit =
|
||||
limits?.fieldArrayIndexLimit ??
|
||||
DEFAULT_FIELD_NAME_LIMITS.fieldArrayIndexLimit;
|
||||
if (
|
||||
typeof limits?.fieldArrayIndexLimit === 'number' &&
|
||||
exceedsArrayIndexLimit(name, limits.fieldArrayIndexLimit)
|
||||
arrayIndexLimit !== Infinity &&
|
||||
exceedsArrayIndexLimit(name, arrayIndexLimit)
|
||||
) {
|
||||
throw new MultipartError('LIMIT_FIELD_ARRAY_INDEX', name);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,21 @@ describe('FileStreamInterceptor', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply the default field name limits to the fields before the file', async () => {
|
||||
const target = new (FileStreamInterceptor('upload'))();
|
||||
await expect(
|
||||
target.intercept(
|
||||
createContext(
|
||||
createFakeRequest([field('a[1001]', 'x'), file('upload')]),
|
||||
createReply(),
|
||||
),
|
||||
handler,
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new BadRequestException('Field name array index too large - a[1001]'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject a file in another field', async () => {
|
||||
const target = new (FileStreamInterceptor('upload'))();
|
||||
const req = createFakeRequest([file('other')]);
|
||||
|
||||
+43
-16
@@ -145,23 +145,50 @@ describe('NoFilesInterceptor', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should leave nesting and array indexes unlimited by default', async () => {
|
||||
const name = 'a[b][c][d][7]';
|
||||
const expected = { a: { b: { c: { d: new Array(7).concat('x') } } } };
|
||||
expect(await intercept(new (NoFilesInterceptor())(), name)).toEqual(
|
||||
expected,
|
||||
/** `{ a: { b: { b: ... value } } }`, for the name `a[b][b]...`. */
|
||||
const nest = (depth: number, value: unknown) => {
|
||||
for (let i = 0; i < depth; i++) {
|
||||
value = { b: value };
|
||||
}
|
||||
return { a: value };
|
||||
};
|
||||
|
||||
it('should limit nesting to 32 levels by default', async () => {
|
||||
const target = new (NoFilesInterceptor())();
|
||||
expect(await intercept(target, 'a' + '[b]'.repeat(32))).toEqual(
|
||||
nest(32, 'x'),
|
||||
);
|
||||
expect(
|
||||
await intercept(
|
||||
new (NoFilesInterceptor({
|
||||
limits: {
|
||||
fieldNestingDepth: Infinity,
|
||||
fieldArrayIndexLimit: Infinity,
|
||||
},
|
||||
}))(),
|
||||
name,
|
||||
),
|
||||
).toEqual(expected);
|
||||
const name = 'a' + '[b]'.repeat(33);
|
||||
await expect(intercept(target, name)).rejects.toEqual(
|
||||
new BadRequestException(`Field name nesting too deep - ${name}`),
|
||||
);
|
||||
});
|
||||
|
||||
it('should limit array indexes to 1000 by default', async () => {
|
||||
const target = new (NoFilesInterceptor())();
|
||||
expect((await intercept(target, 'a[1000]')).a).toHaveLength(1001);
|
||||
await expect(intercept(target, 'a[1001]')).rejects.toEqual(
|
||||
new BadRequestException('Field name array index too large - a[1001]'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply the default field name limits to limits resolved per request', async () => {
|
||||
const target = new (NoFilesInterceptor({ limits: () => ({}) }))();
|
||||
await expect(intercept(target, 'a[1001]')).rejects.toEqual(
|
||||
new BadRequestException('Field name array index too large - a[1001]'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should lift the default field name limits when set to Infinity', async () => {
|
||||
const name = 'a' + '[b]'.repeat(33) + '[1001]';
|
||||
const expected = nest(33, new Array(1001).concat('x'));
|
||||
for (const limits of [
|
||||
{ fieldNestingDepth: Infinity, fieldArrayIndexLimit: Infinity },
|
||||
() => ({ fieldNestingDepth: Infinity, fieldArrayIndexLimit: Infinity }),
|
||||
]) {
|
||||
const target = new (NoFilesInterceptor({ limits }))();
|
||||
expect(await intercept(target, name)).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should check the field name limits in multer's order", async () => {
|
||||
|
||||
Reference in new issue
Block a user