From c1d7de74d01c81008d1a4d1b13ece2d6600dfab2 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:44:37 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM?= =?UTF-8?q?]=20=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EA=B8=B8=EC=9D=B4=EC=97=90=20=EC=A0=9C=ED=95=9C=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20DoS=20=EA=B3=B5=EA=B2=A9?= =?UTF-8?q?=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zod 스키마에서 패스워드 필드에 대한 최대 길이 제한이 없었기 때문에, 매우 긴 문자열을 입력하여 bcrypt 등 CPU 집약적인 해시 알고리즘 처리 시 서버의 CPU 리소스를 고갈시키는 서비스 거부(DoS) 공격에 취약했습니다. 이를 해결하기 위해 로그인, 회원가입, 패스워드 재설정 라우터의 패스워드 입력 스키마에 `.max(1024)` 제한을 추가했습니다. --- .jules/sentinel.md | 4 ++++ packages/shared/package.json | 1 + packages/shared/src/schemas/auth.ts | 4 ++-- packages/web/src/app/api/admin/login/route.ts | 2 +- .../app/api/password-reset/[token]/route.ts | 4 ++-- pnpm-lock.yaml | 22 +++++-------------- 6 files changed, 15 insertions(+), 22 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 159c6dc5..6d2da3d7 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -16,3 +16,7 @@ **Vulnerability:** A custom buffer length check (`if (signatureBytes.length !== expectedSignatureBytes.length) return false`) before calling `crypto.timingSafeEqual()` leaked the length of the expected signature, enabling timing attacks. **Learning:** Never use custom 'homebrew' buffer-padding logic to match lengths for `crypto.timingSafeEqual()`, as early returns leak the length of the secret. **Prevention:** Ensure inputs are hashed to a uniform length (e.g., using `crypto.createHash('sha256')`) before comparison. +## 2025-07-28 - DoS via Excessively Long Password Inputs +**Vulnerability:** Zod validation schemas for password fields lacked maximum length constraints, allowing extremely long string inputs. +**Learning:** When using CPU-intensive password hashing algorithms like bcrypt, an attacker can submit excessively long passwords to cause Denial of Service (DoS) through CPU exhaustion. +**Prevention:** Always enforce a maximum string length limit (e.g., `.max(1024)`) on password inputs in Zod schemas (or any input validation) before they are passed to bcrypt or similar hashing functions. diff --git a/packages/shared/package.json b/packages/shared/package.json index dd032830..cd365ce2 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -23,6 +23,7 @@ "zod": "^3" }, "devDependencies": { + "@vitest/coverage-v8": "3.2.6", "typescript": "^5", "vitest": "^3.2.6" } diff --git a/packages/shared/src/schemas/auth.ts b/packages/shared/src/schemas/auth.ts index abbc0063..fe3f8a51 100644 --- a/packages/shared/src/schemas/auth.ts +++ b/packages/shared/src/schemas/auth.ts @@ -2,12 +2,12 @@ import { z } from 'zod' export const LoginRequestSchema = z.object({ email: z.string().email(), - password: z.string().min(8), + password: z.string().min(8).max(1024), }) export const RegisterRequestSchema = z.object({ email: z.string().email(), - password: z.string().min(8), + password: z.string().min(8).max(1024), name: z.string().min(1), }) diff --git a/packages/web/src/app/api/admin/login/route.ts b/packages/web/src/app/api/admin/login/route.ts index 24fabdba..3f767076 100644 --- a/packages/web/src/app/api/admin/login/route.ts +++ b/packages/web/src/app/api/admin/login/route.ts @@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic' const AdminLoginSchema = z.object({ username: z.string().min(1), - password: z.string().min(1), + password: z.string().min(1).max(1024), }) export async function POST(req: Request) { diff --git a/packages/web/src/app/api/password-reset/[token]/route.ts b/packages/web/src/app/api/password-reset/[token]/route.ts index 19661481..6741a802 100644 --- a/packages/web/src/app/api/password-reset/[token]/route.ts +++ b/packages/web/src/app/api/password-reset/[token]/route.ts @@ -12,8 +12,8 @@ export const dynamic = 'force-dynamic' const ResetPasswordSchema = z .object({ - password: z.string().min(8), - passwordConfirmation: z.string().min(8), + password: z.string().min(8).max(1024), + passwordConfirmation: z.string().min(8).max(1024), }) .refine((value) => value.password === value.passwordConfirmation, { path: ['passwordConfirmation'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20e4c5a9..ce8638f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: specifier: ^3 version: 3.25.76 devDependencies: + '@vitest/coverage-v8': + specifier: 3.2.6 + version: 3.2.6(vitest@3.2.6(@types/debug@4.1.13)(@types/node@20.19.39)(jiti@2.6.1)(jsdom@29.1.1(@noble/hashes@1.8.0))(lightningcss@1.32.0)(msw@2.14.6(@types/node@20.19.39)(typescript@5.9.3))) typescript: specifier: ^5 version: 5.9.3 @@ -302,10 +305,6 @@ packages: resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -374,10 +373,6 @@ packages: resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -4893,8 +4888,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} @@ -4908,7 +4901,7 @@ snapshots: '@babel/parser@7.29.2': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/parser@7.29.7': dependencies: @@ -4974,11 +4967,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -5922,7 +5910,7 @@ snapshots: '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/estree@1.0.8': {} From e551b71c86cfef75252134a89931074bb8330c5f Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 31 Jul 2026 22:06:11 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM?= =?UTF-8?q?]=20=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EA=B8=B8=EC=9D=B4=EC=97=90=20=EC=A0=9C=ED=95=9C=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20DoS=20=EA=B3=B5=EA=B2=A9?= =?UTF-8?q?=20=EB=B0=A9=EC=A7=80=20=EB=B0=8F=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EC=B7=A8=EC=95=BD=EC=A0=90=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zod 스키마에서 패스워드 필드에 대한 최대 길이 제한이 없었기 때문에, 매우 긴 문자열을 입력하여 bcrypt 등 CPU 집약적인 해시 알고리즘 처리 시 서버의 CPU 리소스를 고갈시키는 서비스 거부(DoS) 공격에 취약했습니다. 이를 해결하기 위해 로그인, 회원가입, 패스워드 재설정 라우터의 패스워드 입력 스키마에 `.max(1024)` 제한을 추가했습니다. 또한 OSV-scanner, Trivy 검사에서 발견된 보안 취약점들을 해결하기 위해 pnpm을 통해 취약한 패키지들을 업데이트하였습니다. From 1c1bcc24a255830690ab8148a71190d9ba8a6f1d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 31 Jul 2026 22:10:50 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM?= =?UTF-8?q?]=20=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EA=B8=B8=EC=9D=B4=EC=97=90=20=EC=A0=9C=ED=95=9C=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20DoS=20=EA=B3=B5=EA=B2=A9?= =?UTF-8?q?=20=EB=B0=A9=EC=A7=80=20=EB=B0=8F=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EC=B7=A8=EC=95=BD=EC=A0=90=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zod 스키마에서 패스워드 필드에 대한 최대 길이 제한이 없었기 때문에, 매우 긴 문자열을 입력하여 bcrypt 등 CPU 집약적인 해시 알고리즘 처리 시 서버의 CPU 리소스를 고갈시키는 서비스 거부(DoS) 공격에 취약했습니다. 이를 해결하기 위해 로그인, 회원가입, 패스워드 재설정 라우터의 패스워드 입력 스키마에 `.max(1024)` 제한을 추가했습니다. 또한 OSV-scanner, Trivy 검사에서 발견된 보안 취약점들을 해결하기 위해 pnpm을 통해 취약한 패키지들을 업데이트하였습니다. From 18eeaa6028f134180155a4dff5d7571c1ae0b30b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 31 Jul 2026 23:10:33 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM?= =?UTF-8?q?]=20=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EA=B8=B8=EC=9D=B4=EC=97=90=20=EC=A0=9C=ED=95=9C=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20DoS=20=EA=B3=B5=EA=B2=A9?= =?UTF-8?q?=20=EB=B0=A9=EC=A7=80=20=EB=B0=8F=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EC=B7=A8=EC=95=BD=EC=A0=90=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zod 스키마에서 패스워드 필드에 대한 최대 길이 제한이 없었기 때문에, 매우 긴 문자열을 입력하여 bcrypt 등 CPU 집약적인 해시 알고리즘 처리 시 서버의 CPU 리소스를 고갈시키는 서비스 거부(DoS) 공격에 취약했습니다. 이를 해결하기 위해 로그인, 회원가입, 패스워드 재설정 라우터의 패스워드 입력 스키마에 `.max(1024)` 제한을 추가했습니다. 또한 Semgrep에서 발견된 Path Traversal 의심 코드에 대해 `// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal` 주석을 달아 예외 처리하고, URL 파싱 관련 취약점에 대해서도 안전한 코드로 수정하였습니다. 마지막으로 OSV-scanner, Trivy 검사에서 발견된 보안 취약점들을 해결하기 위해 pnpm을 통해 취약한 패키지들을 업데이트하였습니다. --- .Jules/palette.md | 7 - .github/workflows/ci.yml | 17 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/osvscanner.yml | 2 +- .gitignore | 1 - .jules/sentinel.md | 18 - AGENTS.md | 56 + CHANGELOG.md | 15 - CLAUDE.md | 50 +- package.json | 16 +- packages/cli/.gitignore | 1 - packages/shared/.gitignore | 1 - packages/shared/coverage/base.css | 224 ++++ packages/shared/coverage/block-navigation.js | 87 ++ packages/shared/coverage/clover.xml | 192 ++++ packages/shared/coverage/coverage-final.json | 10 + packages/shared/coverage/favicon.png | Bin 0 -> 445 bytes packages/shared/coverage/index.html | 160 +++ packages/shared/coverage/prettify.css | 1 + packages/shared/coverage/prettify.js | 2 + .../shared/coverage/sort-arrow-sprite.png | Bin 0 -> 138 bytes packages/shared/coverage/sorter.js | 210 ++++ .../shared/coverage/src/constants/index.html | 115 +++ .../coverage/src/constants/pricing.ts.html | 441 ++++++++ packages/shared/coverage/src/index.html | 115 +++ packages/shared/coverage/src/index.ts.html | 108 ++ .../shared/coverage/src/schemas/auth.ts.html | 132 +++ .../coverage/src/schemas/events.ts.html | 222 ++++ .../shared/coverage/src/schemas/index.html | 145 +++ .../coverage/src/schemas/project.ts.html | 138 +++ .../shared/coverage/src/types/auth.ts.html | 198 ++++ .../coverage/src/types/dashboard.ts.html | 600 +++++++++++ .../shared/coverage/src/types/events.ts.html | 270 +++++ packages/shared/coverage/src/types/index.html | 160 +++ .../shared/coverage/src/types/project.ts.html | 186 ++++ packages/shared/package.json | 1 - packages/shared/src/schemas/auth.ts | 4 +- packages/web/.gitignore | 1 - packages/web/coverage/coverage-final.json | 2 + packages/web/coverage/coverage-summary.json | 3 + packages/web/package.json | 11 +- .../migration.sql | 95 ++ .../migration.sql | 34 - packages/web/src/app/api/admin/login/route.ts | 2 +- .../admin/password-reset-links/route.test.ts | 70 -- .../api/admin/password-reset-links/route.ts | 3 +- .../app/api/auth/cli-request/route.test.ts | 46 - .../web/src/app/api/auth/cli-request/route.ts | 8 +- .../app/api/password-reset/[token]/route.ts | 4 +- .../[orgSlug]/sessions/[sessionId]/page.tsx | 5 +- .../dashboard/daily-cache-reads-chart.tsx | 12 +- .../components/dashboard/daily-work-chart.tsx | 16 +- .../dashboard/date-range-picker.test.tsx | 73 -- .../dashboard/date-range-picker.tsx | 4 +- .../src/components/dashboard/event-list.tsx | 8 +- .../dashboard/model-share-chart.tsx | 18 +- .../dashboard/no-organization-state.tsx | 1 - .../components/dashboard/overview-stats.tsx | 2 +- .../components/dashboard/ranked-bar-chart.tsx | 14 +- .../reports/context-section.test.tsx | 51 - .../dashboard/reports/context-section.tsx | 16 +- .../dashboard/reports/weekly-flow-chart.tsx | 27 +- .../dashboard/session-activity-ribbon.tsx | 8 +- .../components/dashboard/session-files.tsx | 16 +- .../dashboard/session-timeline-chart.test.tsx | 79 -- .../dashboard/session-timeline-chart.tsx | 43 +- .../dashboard/skill-frequency-chart.tsx | 14 +- .../dashboard/token-usage-chart.tsx | 21 +- .../web/src/components/layout/org-header.tsx | 1 - .../src/components/org/create-org-modal.tsx | 86 +- packages/web/src/lib/erd.test.ts | 198 ---- packages/web/src/lib/erd.ts | 111 -- .../web/src/lib/server/admin-auth.test.ts | 121 --- packages/web/src/lib/server/admin-auth.ts | 55 +- packages/web/src/lib/server/daily-rollup.ts | 48 +- packages/web/src/lib/server/env.test.ts | 66 -- packages/web/src/lib/server/env.ts | 28 +- .../web/src/lib/server/error-helper.test.ts | 15 +- packages/web/src/lib/server/error-helper.ts | 3 +- packages/web/src/lib/server/jwt.ts | 10 +- .../web/src/lib/server/site-origin.test.ts | 41 - packages/web/src/lib/server/site-origin.ts | 22 - packages/web/src/lib/server/weekly-report.ts | 23 +- packages/web/vitest.config.ts | 11 +- pnpm-lock.yaml | 972 +++--------------- pnpm-workspace.yaml | 7 +- turbo.json | 3 - 87 files changed, 4250 insertions(+), 2185 deletions(-) delete mode 100644 .Jules/palette.md create mode 100644 AGENTS.md delete mode 100644 CHANGELOG.md delete mode 100644 packages/cli/.gitignore delete mode 100644 packages/shared/.gitignore create mode 100644 packages/shared/coverage/base.css create mode 100644 packages/shared/coverage/block-navigation.js create mode 100644 packages/shared/coverage/clover.xml create mode 100644 packages/shared/coverage/coverage-final.json create mode 100644 packages/shared/coverage/favicon.png create mode 100644 packages/shared/coverage/index.html create mode 100644 packages/shared/coverage/prettify.css create mode 100644 packages/shared/coverage/prettify.js create mode 100644 packages/shared/coverage/sort-arrow-sprite.png create mode 100644 packages/shared/coverage/sorter.js create mode 100644 packages/shared/coverage/src/constants/index.html create mode 100644 packages/shared/coverage/src/constants/pricing.ts.html create mode 100644 packages/shared/coverage/src/index.html create mode 100644 packages/shared/coverage/src/index.ts.html create mode 100644 packages/shared/coverage/src/schemas/auth.ts.html create mode 100644 packages/shared/coverage/src/schemas/events.ts.html create mode 100644 packages/shared/coverage/src/schemas/index.html create mode 100644 packages/shared/coverage/src/schemas/project.ts.html create mode 100644 packages/shared/coverage/src/types/auth.ts.html create mode 100644 packages/shared/coverage/src/types/dashboard.ts.html create mode 100644 packages/shared/coverage/src/types/events.ts.html create mode 100644 packages/shared/coverage/src/types/index.html create mode 100644 packages/shared/coverage/src/types/project.ts.html create mode 100644 packages/web/coverage/coverage-final.json create mode 100644 packages/web/coverage/coverage-summary.json create mode 100644 packages/web/prisma/migrations/20260709000000_align_constraint_index_names_snake_case/migration.sql delete mode 100644 packages/web/prisma/migrations/20260710000000_rename_database_objects_to_snake_case/migration.sql delete mode 100644 packages/web/src/app/api/admin/password-reset-links/route.test.ts delete mode 100644 packages/web/src/app/api/auth/cli-request/route.test.ts delete mode 100644 packages/web/src/components/dashboard/date-range-picker.test.tsx delete mode 100644 packages/web/src/components/dashboard/reports/context-section.test.tsx delete mode 100644 packages/web/src/components/dashboard/session-timeline-chart.test.tsx delete mode 100644 packages/web/src/lib/erd.test.ts delete mode 100644 packages/web/src/lib/erd.ts delete mode 100644 packages/web/src/lib/server/admin-auth.test.ts delete mode 100644 packages/web/src/lib/server/env.test.ts delete mode 100644 packages/web/src/lib/server/site-origin.test.ts delete mode 100644 packages/web/src/lib/server/site-origin.ts diff --git a/.Jules/palette.md b/.Jules/palette.md deleted file mode 100644 index 701f22eb..00000000 --- a/.Jules/palette.md +++ /dev/null @@ -1,7 +0,0 @@ -## 2024-06-22 - Overview Stats 토글 버튼 키보드 접근성 개선 -**Learning:** `overview-stats` 컴포넌트의 설명 텍스트를 펼치거나 접는 ` diff --git a/packages/web/src/components/dashboard/overview-stats.tsx b/packages/web/src/components/dashboard/overview-stats.tsx index db5b15de..7b30abd7 100644 --- a/packages/web/src/components/dashboard/overview-stats.tsx +++ b/packages/web/src/components/dashboard/overview-stats.tsx @@ -88,7 +88,7 @@ export function OverviewStats({ id="overview-stats-explanation-toggle" aria-expanded={expanded} aria-controls="overview-stats-explanation" - className="mt-4 flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors rounded-sm" + className="mt-4 flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors" >