Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,13 @@
**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-08-04 - [DDL 생성 μ‹œ SQL μΈμ μ…˜ λ°©μ§€]
**Vulnerability:** ERD λ„κ΅¬μ—μ„œ DDL을 생성할 λ•Œ, 컬럼 νƒ€μž… λ¬Έμžμ—΄μ— μ„Έλ―Έμ½œλ‘ (;)을 ν¬ν•¨μ‹œμΌœ κΈ°μ‘΄ λ¬Έμž₯을 μ’…λ£Œν•˜κ³  μ•…μ˜μ μΈ SQL λ¬Έ(예: `DROP TABLE`)을 μ‚½μž…ν•  수 μžˆλŠ” SQL μΈμ μ…˜ 취약점이 μ‘΄μž¬ν–ˆμŠ΅λ‹ˆλ‹€.
**Learning:** μ‚¬μš©μž μž…λ ₯(이 경우 컬럼 νƒ€μž…)을 직접 μ—°κ²°ν•˜μ—¬ SQL μΏΌλ¦¬λ‚˜ DDL을 μƒμ„±ν•˜λŠ” 경우, ꡬ문 μ’…κ²°μž(Statement Terminator)λ₯Ό ν•„ν„°λ§ν•˜μ§€ μ•ŠμœΌλ©΄ λ°μ΄ν„°λ² μ΄μŠ€ μ‘°μž‘μœΌλ‘œ μ΄μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.
**Prevention:** DDLκ³Ό 같은 동적 쿼리λ₯Ό 생성할 λ•Œ ꡬ문 μ’…κ²°μžκ°€ μž…λ ₯에 ν¬ν•¨λ˜μ–΄ μžˆλŠ”μ§€ 확인(`assertNoStatementTerminator`)ν•˜μ—¬ μ•…μ˜μ μΈ SQL λ¬Έ 싀행을 사전에 차단해야 ν•©λ‹ˆλ‹€.

## 2025-08-04 - [ERD λͺ¨λΈμ˜ 심화 SQL μΈμ μ…˜ 및 μƒνƒœ μ‘°μž‘ λ°©μ§€]
**Vulnerability:** 기쑴의 μ„Έλ―Έμ½œλ‘  κ²€μ‚¬λ§ŒμœΌλ‘œλŠ” SQL 주석(`--`, `/*`)μ΄λ‚˜ `UNION` 곡격을 ν†΅ν•œ SQL μΈμ μ…˜μ„ 막을 수 μ—†μ—ˆμœΌλ©°, `getTable()` 등을 톡해 λ‚΄λΆ€ μƒνƒœ 객체가 참쑰둜 λ°˜ν™˜λ˜μ–΄ μœ νš¨μ„± 검사 이후에 μž„μ˜λ‘œ μ•…μ„± SQL을 μ£Όμž…(Prototype Pollution/State Manipulation)ν•  수 μžˆλŠ” 취약점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
**Learning:** λ‚΄λΆ€ μƒνƒœλ₯Ό μ™ΈλΆ€λ‘œ λ…ΈμΆœν•  λ•Œ μ°Έμ‘°ν˜• 객체λ₯Ό κ·ΈλŒ€λ‘œ λ°˜ν™˜ν•˜λ©΄ μ™ΈλΆ€μ—μ„œ μƒνƒœλ₯Ό μ‘°μž‘ν•˜μ—¬ λͺ¨λ“  λ‚΄λΆ€ λ³΄μ•ˆ 검증을 μš°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ SQL μΈμ μ…˜μ€ μ„Έλ―Έμ½œλ‘  외에도 주석 및 μ˜ˆμ•½μ–΄(UNION λ“±)λ₯Ό μ΄μš©ν•œ 곡격 κ²½λ‘œκ°€ μ‘΄μž¬ν•©λ‹ˆλ‹€.
**Prevention:** 컬럼 νƒ€μž…κ³Ό 같은 μ‚¬μš©μž μž…λ ₯은 μ—„κ²©ν•œ ν™”μ΄νŠΈλ¦¬μŠ€νŠΈλ‚˜ μƒμ„Έν•œ 차단 λͺ©λ‘(주석, UNION λ“±)으둜 검증해야 ν•˜λ©°, 객체 μƒνƒœλ₯Ό λ°˜ν™˜ν•  λ•ŒλŠ” 볡사본(예: κΉŠμ€ 볡사 `JSON.parse(JSON.stringify(obj))`)을 λ°˜ν™˜ν•˜μ—¬ λ‚΄λΆ€ λ§΅μ΄λ‚˜ 속성을 직접 μ‘°μž‘ν•  수 없도둝 격리해야 ν•©λ‹ˆλ‹€.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@
"@babel/core": "7.29.7",
"esbuild": "0.28.1",
"js-yaml": "^4.3.0",
"fast-uri": "^3.1.4",
"fast-uri": "3.1.5",
"@auth/core": "^0.41.3",
"sharp": "^0.35.0",
"postcss": "^8.5.18",
"hono": "^4.12.27",
"brace-expansion": "5.0.8",
"hono": "4.13.0",
"brace-expansion": "5.0.9",
"minimatch": "^10.0.0",
"@hono/node-server": "^2.0.5",
"body-parser": "^2.3.0"
"body-parser": "^2.3.0",
"ip-address": "10.4.0",
"undici": "7.29.0"
}
}
}
37 changes: 36 additions & 1 deletion packages/web/src/lib/erd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -33,6 +33,17 @@ describe('ERDModel', () => {
it('should return undefined for non-existent table', () => {
expect(model.getTable('non_existent')).toBeUndefined()
})

it('should return deeply copied objects from getTable to prevent prototype pollution', () => {
model.addTable('users')
model.addColumn('users', { name: 'id', type: 'integer' })
const table = model.getTable('users')
if (table) {
table.columns[0].type = 'integer PRIMARY KEY; DROP TABLE users; --'
}
const unchangedTable = model.getTable('users')
expect(unchangedTable?.columns[0].type).toBe('integer')
})
})

describe('Column Management', () => {
Expand Down Expand Up @@ -67,6 +78,30 @@ describe('ERDModel', () => {
model.addColumn('users', { name: 'created__at', type: 'timestamp' })
).toThrowError("Column 'created__at' must be snake_case.")
})

it('should reject column types containing statement terminators', () => {
model.addTable('users')
expect(() =>
model.addColumn('users', { name: 'id', type: 'integer PRIMARY KEY); DROP TABLE users; --' })
).toThrowError('Statement terminators are not allowed to prevent SQL injection.')
})

it('should reject column types containing SQL comments', () => {
model.addTable('users')
expect(() =>
model.addColumn('users', { name: 'id', type: 'integer PRIMARY KEY -- injected comment' })
).toThrowError('SQL comments are not allowed in column types.')
expect(() =>
model.addColumn('users', { name: 'id', type: 'integer /* comment */ PRIMARY KEY' })
).toThrowError('SQL comments are not allowed in column types.')
})

it('should reject column types containing UNION attacks', () => {
model.addTable('users')
expect(() =>
model.addColumn('users', { name: 'id', type: 'integer UNION SELECT password FROM users' })
).toThrowError('UNION statements are not allowed in column types.')
})
})

describe('Foreign Key Management', () => {
Expand Down
18 changes: 16 additions & 2 deletions packages/web/src/lib/erd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ function assertSnakeCaseIdentifier(kind: string, name: string): void {
}
}

function assertValidSqlType(value: string): void {
if (value.includes(';')) {
throw new Error('Statement terminators are not allowed to prevent SQL injection.')
}
if (value.includes('--') || value.includes('/*')) {
throw new Error('SQL comments are not allowed in column types.')
}
if (/UNION/i.test(value)) {
throw new Error('UNION statements are not allowed in column types.')
}
}

export class ERDModel {
private tables: Map<string, Table> = new Map()

Expand All @@ -39,16 +51,18 @@ export class ERDModel {
}

getTable(name: string): Table | undefined {
return this.tables.get(name)
const table = this.tables.get(name)
return table ? JSON.parse(JSON.stringify(table)) : undefined
}

getTables(): Table[] {
return Array.from(this.tables.values())
return Array.from(this.tables.values()).map(table => JSON.parse(JSON.stringify(table)))
}

addColumn(tableName: string, column: Column): void {
assertSnakeCaseIdentifier('Table', tableName)
assertSnakeCaseIdentifier('Column', column.name)
assertValidSqlType(column.type)
const table = this.tables.get(tableName)
if (!table) {
throw new Error(`Table '${tableName}' does not exist.`)
Expand Down
56 changes: 29 additions & 27 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading