From 861c8efa6b4ca7e0919733b0bfd026d586f0d3a6 Mon Sep 17 00:00:00 2001 From: ttipo Date: Sun, 21 Jun 2026 01:27:41 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8E=B8=EC=9D=98=EB=A5=BC=20=EC=9C=84=ED=95=B4=20rate=20limit?= =?UTF-8?q?=20=EC=9D=84=20=EB=B6=84=EB=8B=B9=201000=ED=9A=8C=EB=A1=9C=20?= =?UTF-8?q?=EC=99=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 세 겹의 제한을 모두 분당 1000회로 상향(테스트 중 429 회피): - main.ts express-rate-limit: 15분 150회 → 1분 1000회 - 전역 ThrottlerModule: 분당 100회 → 1000회 - 라우트별 @Throttle(login/signup/resend/forgot/reset/ai): 3~20회 → 1000회 각 라우트 주석에 운영 권장값을 남겨 추후 복원/ env 화 용이. 운영 배포 전 적정값으로 되돌릴 것. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/app.module.ts | 3 ++- backend/src/main.ts | 5 +++-- .../src/modules/ai/ai-suggest.controller.ts | 2 +- backend/src/modules/ai/ai.controller.ts | 4 ++-- backend/src/modules/auth/auth.controller.ts | 20 +++++++++---------- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 9504d9e..1806eff 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -57,10 +57,11 @@ import { ProjectModule } from './modules/project/project.module'; }, }), // 전역 Rate Limiter — main.ts 의 express-rate-limit 와 별개로 라우트 단위 제어 가능 + // NOTE: 개발/테스트 편의를 위해 분당 1000회로 완화함. 운영 배포 전 적정값으로 되돌릴 것. ThrottlerModule.forRoot([ { ttl: 60_000, - limit: 100, + limit: 1000, }, ]), TypeOrmModule.forRootAsync({ diff --git a/backend/src/main.ts b/backend/src/main.ts index 56cbe3f..305fda1 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -70,10 +70,11 @@ async function bootstrap() { }); // 3. 글로벌 Rate Limiting 적용 (DDoS, 브루트포스 예방) + // NOTE: 개발/테스트 편의를 위해 분당 1000회로 완화함. 운영 배포 전 적정값으로 되돌리거나 env 화 필요. app.use( rateLimit({ - windowMs: 15 * 60 * 1000, // 15분 - max: 150, // 15분 IP당 최대 150개 요청 + windowMs: 60 * 1000, // 1분 + max: 1000, // 1분 IP당 최대 1000개 요청 message: { success: false, error: { diff --git a/backend/src/modules/ai/ai-suggest.controller.ts b/backend/src/modules/ai/ai-suggest.controller.ts index 96ee902..cb0846c 100644 --- a/backend/src/modules/ai/ai-suggest.controller.ts +++ b/backend/src/modules/ai/ai-suggest.controller.ts @@ -23,7 +23,7 @@ export class AiSuggestController { @Post('suggest-checklist') @HttpCode(HttpStatus.OK) - @Throttle({ default: { limit: 20, ttl: 60_000 } }) + @Throttle({ default: { limit: 1000, ttl: 60_000 } }) @ApiOperation({ summary: '할 일(체크리스트) 추천 — 프로젝트·업무 기반' }) @ApiResponse({ status: 200, description: '카테고리별 추천 항목' }) @ApiResponse({ status: 404, description: '프로젝트 없음 (RES_001)' }) diff --git a/backend/src/modules/ai/ai.controller.ts b/backend/src/modules/ai/ai.controller.ts index 45bbede..3f17fc5 100644 --- a/backend/src/modules/ai/ai.controller.ts +++ b/backend/src/modules/ai/ai.controller.ts @@ -58,7 +58,7 @@ export class AiController { @Post() @HttpCode(HttpStatus.CREATED) - @Throttle({ default: { limit: 20, ttl: 60_000 } }) + @Throttle({ default: { limit: 1000, ttl: 60_000 } }) @ApiOperation({ summary: '새 대화 시작 (첫 메시지)' }) @ApiResponse({ status: 201, description: '생성 + AI 응답' }) start( @@ -70,7 +70,7 @@ export class AiController { @Post(':id/messages') @HttpCode(HttpStatus.OK) - @Throttle({ default: { limit: 20, ttl: 60_000 } }) + @Throttle({ default: { limit: 1000, ttl: 60_000 } }) @ApiOperation({ summary: '대화에 메시지 추가' }) @ApiResponse({ status: 200, description: 'AI 응답' }) @ApiResponse({ status: 404, description: '대화 없음 (RES_001)' }) diff --git a/backend/src/modules/auth/auth.controller.ts b/backend/src/modules/auth/auth.controller.ts index b482b89..aeab156 100644 --- a/backend/src/modules/auth/auth.controller.ts +++ b/backend/src/modules/auth/auth.controller.ts @@ -95,8 +95,8 @@ export class AuthController { @Post('signup') @HttpCode(HttpStatus.CREATED) - // 가입 남용/메일 폭탄 방지 — IP당 분당 5회 - @Throttle({ default: { limit: 5, ttl: 60_000 } }) + // 가입 남용/메일 폭탄 방지 — 운영 권장 5회/분 (현재 테스트용 1000회로 완화) + @Throttle({ default: { limit: 1000, ttl: 60_000 } }) @ApiOperation({ summary: '회원가입 (인증 메일 발송, 자동 로그인 안 함)' }) @ApiResponse({ status: 201, description: '가입 접수 — 인증 메일 발송됨' }) @ApiResponse({ status: 409, description: '이미 가입된 이메일 (BIZ_001)' }) @@ -107,8 +107,8 @@ export class AuthController { @Post('resend-verification') @HttpCode(HttpStatus.OK) - // 메일 폭탄 방지 — IP당 분당 3회 - @Throttle({ default: { limit: 3, ttl: 60_000 } }) + // 메일 폭탄 방지 — 운영 권장 3회/분 (현재 테스트용 1000회로 완화) + @Throttle({ default: { limit: 1000, ttl: 60_000 } }) @ApiOperation({ summary: '인증 메일 재발송' }) @ApiResponse({ status: 200, description: '재발송 접수(존재 여부 비노출)' }) async resend(@Body() dto: ResendVerificationDto): Promise { @@ -118,8 +118,8 @@ export class AuthController { @Post('forgot-password') @HttpCode(HttpStatus.OK) - // 메일 폭탄/계정 탐색 방지 — IP당 분당 3회 - @Throttle({ default: { limit: 3, ttl: 60_000 } }) + // 메일 폭탄/계정 탐색 방지 — 운영 권장 3회/분 (현재 테스트용 1000회로 완화) + @Throttle({ default: { limit: 1000, ttl: 60_000 } }) @ApiOperation({ summary: '비밀번호 재설정 요청 (재설정 메일 발송)' }) @ApiResponse({ status: 200, description: '요청 접수(존재 여부 비노출)' }) async forgotPassword(@Body() dto: ForgotPasswordDto): Promise { @@ -129,8 +129,8 @@ export class AuthController { @Post('reset-password') @HttpCode(HttpStatus.OK) - // 토큰 추측 방지 — IP당 분당 5회 - @Throttle({ default: { limit: 5, ttl: 60_000 } }) + // 토큰 추측 방지 — 운영 권장 5회/분 (현재 테스트용 1000회로 완화) + @Throttle({ default: { limit: 1000, ttl: 60_000 } }) @ApiOperation({ summary: '비밀번호 재설정 (토큰 + 새 비밀번호)' }) @ApiResponse({ status: 200, description: '비밀번호 변경 성공' }) @ApiResponse({ status: 400, description: '토큰 만료/무효 (BIZ_001)' }) @@ -158,8 +158,8 @@ export class AuthController { @Post('login') @HttpCode(HttpStatus.OK) - // 무차별 대입 방지 — IP당 분당 10회 - @Throttle({ default: { limit: 10, ttl: 60_000 } }) + // 무차별 대입 방지 — 운영 권장 10회/분 (현재 테스트용 1000회로 완화) + @Throttle({ default: { limit: 1000, ttl: 60_000 } }) @ApiOperation({ summary: '로그인' }) @ApiResponse({ status: 200, description: '로그인 성공, 인증 쿠키 발급' }) @ApiResponse({