diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 05da88a1..424ceb41 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -20,3 +20,13 @@ **Vulnerability:** The standard user authentication routes (login, register, and reset-password) did not have a maximum length constraint on passwords. This allows an attacker to supply extremely long strings, which `bcrypt` will try to hash, causing CPU exhaustion and creating a Denial of Service (DoS) vulnerability. **Learning:** `bcrypt` (and `bcryptjs`) is intentionally slow. While `bcrypt` may internally truncate passwords to 72 bytes, depending on the implementation the input string processing itself or the full string parsing before truncation can be very costly. In this codebase, the admin authentication correctly checked for a max length, but user schemas did not. **Prevention:** Always enforce a maximum string length limit (e.g. `.max(1024)`) on user inputs that will be passed into expensive algorithms like bcrypt hashing. + +## 2025-08-07 - Mutation Bypass를 통한 SQL Injection 취약점 방지 +**Vulnerability:** `packages/web/src/lib/erd.ts` 내의 `ERDModel` 클래스에서 `getTable`, `addTable`, `addColumn`, `addForeignKey` 등의 메서드가 객체의 참조(reference)를 반환하거나 내부 배열에 그대로 저장하여, 외부에서 객체의 프로퍼티(예: `table.name`, `column.type`)를 임의로 변조(mutation)할 수 있는 취약점이 존재했습니다. 이를 통해 SQL Injection 필터링을 우회하고 악의적인 DDL 문을 주입할 수 있었습니다. +**Learning:** 식별자에 대한 유효성 검사(Snake Case 등)를 수행하더라도, 상태 객체가 참조로 노출되면 언제든지 검증 이후에 값을 변조하여 SQL 주입 공격을 수행할 수 있습니다. +**Prevention:** 상태를 관리하는 클래스에서 객체를 반환하거나 내부 배열에 추가할 때는 항상 `structuredClone()` 등을 사용하여 깊은 복사(deep copy)를 수행하여 외부 변조를 차단해야 합니다. + +## 2026-08-07 - OSV-Scanner 취약점 탐지 및 의존성 강제 고정 +**Vulnerability:** 의존성 스캔 중 `js-yaml`과 `nanoid` 패키지에서 알려진 보안 취약점이 발견되었습니다. 해당 패키지들은 다른 패키지의 하위 의존성(transitive dependency)으로 설치되고 있었습니다. +**Learning:** 직접 설치하지 않은 하위 의존성이라도 전체 애플리케이션의 보안에 영향을 미칠 수 있으며, 의존성 트리 깊은 곳에 있는 취약점은 발견 및 수정이 어려울 수 있습니다. +**Prevention:** `pnpm audit` 또는 `osv-scanner`와 같은 도구를 CI 파이프라인에 통합하여 지속적으로 스캔하고, 취약점이 발견될 경우 `package.json`의 `pnpm.overrides`를 활용하여 안전한 패치 버전으로 강제 고정(override)하여 선제적으로 방어해야 합니다. diff --git a/package.json b/package.json index 70cf24f1..cfd6b934 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "@babel/core": "7.29.7", "esbuild": "0.28.1", "hono": "^4.12.34", - "js-yaml": "^4.3.0", + "js-yaml": "^4.3.1", + "nanoid": "^3.3.18", "@auth/core": "^0.41.3", "sharp": "^0.35.3", "postcss": "^8.5.18", diff --git a/packages/web/src/lib/erd.test.ts b/packages/web/src/lib/erd.test.ts index 0ddcf189..7a3c0d2d 100644 --- a/packages/web/src/lib/erd.test.ts +++ b/packages/web/src/lib/erd.test.ts @@ -13,7 +13,7 @@ describe('ERDModel', () => { const table = model.addTable('users') expect(table.name).toBe('users') expect(model.getTables().length).toBe(1) - expect(model.getTable('users')).toBe(table) + expect(model.getTable('users')).toStrictEqual(table) }) it('should throw when adding duplicate table', () => { diff --git a/packages/web/src/lib/erd.ts b/packages/web/src/lib/erd.ts index 046a09c5..60b70250 100644 --- a/packages/web/src/lib/erd.ts +++ b/packages/web/src/lib/erd.ts @@ -35,15 +35,16 @@ export class ERDModel { } const table: Table = { name, columns: [], foreignKeys: [] } this.tables.set(name, table) - return table + return structuredClone(table) } getTable(name: string): Table | undefined { - return this.tables.get(name) + const t = this.tables.get(name) + return t ? structuredClone(t) : undefined } getTables(): Table[] { - return Array.from(this.tables.values()) + return Array.from(this.tables.values()).map(t => structuredClone(t)) } addColumn(tableName: string, column: Column): void { @@ -56,7 +57,7 @@ export class ERDModel { if (table.columns.some((c) => c.name === column.name)) { throw new Error(`Column '${column.name}' already exists in table '${tableName}'.`) } - table.columns.push(column) + table.columns.push(structuredClone(column)) } addForeignKey(tableName: string, fk: ForeignKey): void { @@ -80,7 +81,7 @@ export class ERDModel { `Reference column '${fk.referenceColumn}' does not exist in table '${fk.referenceTable}'.` ) } - table.foreignKeys.push(fk) + table.foreignKeys.push(structuredClone(fk)) } generateDDL(): string { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9bb1c0e..2bbbb462 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,8 @@ overrides: '@babel/core': 7.29.7 esbuild: 0.28.1 hono: ^4.12.34 - js-yaml: ^4.3.0 + js-yaml: ^4.3.1 + nanoid: ^3.3.18 '@auth/core': ^0.41.3 sharp: ^0.35.3 postcss: ^8.5.18 @@ -3192,8 +3193,8 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true jsdom@29.1.1: @@ -3602,8 +3603,8 @@ packages: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -5204,7 +5205,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 minimatch: 10.2.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -6546,7 +6547,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 parse-json: 5.2.0 optionalDependencies: typescript: 5.9.3 @@ -7754,7 +7755,7 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@4.3.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -8350,7 +8351,7 @@ snapshots: mute-stream@3.0.0: {} - nanoid@3.3.16: {} + nanoid@3.3.18: {} napi-postinstall@0.3.4: {} @@ -8611,7 +8612,7 @@ snapshots: postcss@8.5.25: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1