generated from ttipo/project-base
fix(backend): JSON 본문 모양 한도 · UTF-8 만 — 본문 검증 DoS
본문 크기 한도 안에서도 작은 객체 수만 개면 DTO 변환 · 검사가 서버를 수 초 멈추고 수백 MB 를 썼다(리뷰 측정: 1MB 필터 본문 7.8초 · +329MB, 깊은 중첩은 500 SYS_001, 4MB 발송 본문은 한 건으로 메모리 초과 추정). DTO 의 개수 한도로는 막지 못한다 — 변환은 항목마다 객체를 만들고, 검사는 한도에 걸려도 항목마다 중첩 검사를 한다. - 모든 JSON 본문을 DTO 보다 앞에서 잰다(배열 1,000 · 값 10,000 · 깊이 32). 읽기 전에 UTF-8 바이트를 한 번 훑고, 읽은 뒤에 한 번 더 본다. 넘으면 400 VAL_001 - JSON 은 UTF-8 만 받는다. 다른 utf-* 본문은 바이트로 셀 수 없다 - 전역 파이프에 stopAtFirstError 를 켠다. 항목 수의 제곱이던 ArrayUnique 대신 Set 으로 보는 ArrayDistinct 를 쓴다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
} from '@/modules/auth/auth.constants';
|
||||
import { ChangePasswordDto } from './change-password.dto';
|
||||
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts) — 걸린 규칙 이름만 */
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts — stopAtFirstError 만 빼고 — 걸린 규칙을 모두 본다, 통과 · 실패는 같다) — 걸린 규칙 이름만 */
|
||||
async function constraintsOf(body: object): Promise<string[]> {
|
||||
const errors = await validate(plainToInstance(ChangePasswordDto, body), {
|
||||
whitelist: true,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { validate } from 'class-validator';
|
||||
import { MAX_BULK_MESSAGE_IDS } from '@/modules/mail/mail.constants';
|
||||
import { MessageDeletionDto, UpdateMessagesDto } from './bulk-messages.dto';
|
||||
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts) */
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts — stopAtFirstError 만 빼고 — 걸린 규칙을 모두 본다, 통과 · 실패는 같다) */
|
||||
async function constraintsOf(
|
||||
type: typeof UpdateMessagesDto | typeof MessageDeletionDto,
|
||||
body: object,
|
||||
@@ -47,7 +47,7 @@ describe('여러 통 요청 본문 (PATCH /api/messages · POST /api/message-del
|
||||
).toEqual(['arrayMaxSize']);
|
||||
expect(
|
||||
await constraintsOf(type, { ids: ['m1', 'm1'], ...changes }),
|
||||
).toEqual(['arrayUnique']);
|
||||
).toEqual(['arrayDistinct']);
|
||||
for (const bad of ['../m1', '', 'm 1', 1, null]) {
|
||||
expect(
|
||||
await constraintsOf(type, { ids: ['m1', bad], ...changes }),
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize,
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
Matches,
|
||||
} from 'class-validator';
|
||||
import { ArrayMaxSize, ArrayMinSize, IsArray, Matches } from 'class-validator';
|
||||
|
||||
import { ArrayDistinct } from '@/common/decorators/array-distinct.decorator';
|
||||
import {
|
||||
MAIL_ID_PATTERN,
|
||||
MAX_BULK_MESSAGE_IDS,
|
||||
@@ -33,7 +28,7 @@ function MessageIdList(description: string) {
|
||||
ArrayMaxSize(MAX_BULK_MESSAGE_IDS, {
|
||||
message: `메일은 한 번에 ${MAX_BULK_MESSAGE_IDS}통까지 선택할 수 있습니다.`,
|
||||
}),
|
||||
ArrayUnique({ message: '같은 메일을 두 번 선택했습니다.' }),
|
||||
ArrayDistinct({ message: '같은 메일을 두 번 선택했습니다.' }),
|
||||
Matches(MAIL_ID_PATTERN, {
|
||||
each: true,
|
||||
message: '메일을 다시 선택해 주세요.',
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@/modules/mail/mail.constants';
|
||||
import { MailVacationDto, isUtcDateTime } from './mail-vacation.dto';
|
||||
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts) — 걸린 규칙 이름만 */
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts — stopAtFirstError 만 빼고 — 걸린 규칙을 모두 본다, 통과 · 실패는 같다) — 걸린 규칙 이름만 */
|
||||
async function constraintsOf(body: object): Promise<string[]> {
|
||||
const errors = await validate(plainToInstance(MailVacationDto, body), {
|
||||
whitelist: true,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
RenameMailboxDto,
|
||||
} from './mailbox.dto';
|
||||
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts) — 변환한 값과 걸린 규칙 이름 */
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts — stopAtFirstError 만 빼고 — 걸린 규칙을 모두 본다, 통과 · 실패는 같다) — 변환한 값과 걸린 규칙 이름 */
|
||||
async function check<T extends object>(
|
||||
type: ClassConstructor<T>,
|
||||
body: object,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@/modules/mail/mail.constants';
|
||||
import { MessageListQueryDto } from './message-list-query.dto';
|
||||
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 변환·검증한다 (app.setup.ts — transform · whitelist · forbidNonWhitelisted) */
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 변환·검증한다 (app.setup.ts — transform · whitelist · forbidNonWhitelisted, stopAtFirstError 만 빼고 — 걸린 규칙을 모두 본다, 통과 · 실패는 같다) */
|
||||
async function check(
|
||||
query: Record<string, unknown>,
|
||||
): Promise<{ dto: MessageListQueryDto; constraints: string[] }> {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@/modules/mail/mail.constants';
|
||||
import { SendMessageDto } from './send-message.dto';
|
||||
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts) */
|
||||
/** 전역 ValidationPipe 와 같은 옵션으로 검증한다 (app.setup.ts — stopAtFirstError 만 빼고 — 걸린 규칙을 모두 본다, 통과 · 실패는 같다) */
|
||||
async function constraintsOf(body: object): Promise<string[]> {
|
||||
const dto = plainToInstance(SendMessageDto, body);
|
||||
const errors = await validate(dto, {
|
||||
|
||||
Reference in New Issue
Block a user