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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@
**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.

## 2024-10-24 - [SQL Injection λ°©μ§€]
**Vulnerability:** ERD λ„κ΅¬μ˜ DDL 생성 μ‹œ 컬럼 νƒ€μž…(column.type)에 λŒ€ν•œ 검증이 μ—†μ–΄ SQL μΈμ μ…˜(예: μ„Έλ―Έμ½œλ‘ μ„ ν†΅ν•œ 쿼리 μ’…λ£Œ 및 μ•…μ„± 쿼리 μ‹€ν–‰) 취약점이 μ‘΄μž¬ν•¨.
**Learning:** μ‹λ³„μž(ν…Œμ΄λΈ”λͺ…, 컬럼λͺ…)에 λŒ€ν•΄μ„œλŠ” snake_case 검증이 μžˆμ—ˆμœΌλ‚˜, 자유 ν˜•μ‹μ΄ ν—ˆμš©λ˜λŠ” SQL νƒ€μž…μ— λŒ€ν•΄μ„œλŠ” 검증이 λˆ„λ½λ˜μ–΄ λ°œμƒν•¨.
**Prevention:** μ‚¬μš©μž μž…λ ₯이 DDL λ¬Έμžμ—΄μ— 직접 μ‚½μž…λ  λ•ŒλŠ” μ„Έλ―Έμ½œλ‘ (;)κ³Ό 같은 ꡬ문 μ’…λ£Œ 문자λ₯Ό μ—„κ²©νžˆ 차단해야 함.
24 changes: 24 additions & 0 deletions packages/web/src/lib/erd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ describe('ERDModel', () => {
model.addColumn('users', { name: 'created__at', type: 'timestamp' })
).toThrowError("Column 'created__at' must be snake_case.")
})

it('should reject unsafe SQL type syntax while preserving legitimate parameters', () => {
model.addTable('users')

expect(() =>
model.addColumn('users', { name: 'injected_column', type: 'integer, admin boolean' })
).toThrowError("Invalid SQL type 'integer, admin boolean'.")
expect(() =>
model.addColumn('users', { name: 'commented_type', type: 'integer -- comment' })
).toThrowError("Invalid SQL type 'integer -- comment'.")
expect(() =>
model.addColumn('users', { name: 'unterminated_type', type: 'numeric(10,2' })
).toThrowError("Invalid SQL type 'numeric(10,2'.")
expect(() =>
model.addColumn('users', { name: 'terminated_type', type: 'integer;' })
).toThrowError("Invalid SQL type 'integer;'.")

expect(() =>
model.addColumn('users', { name: 'amount', type: 'numeric(10,2)' })
).not.toThrow()
expect(() =>
model.addColumn('users', { name: 'recorded_at', type: 'timestamp with time zone' })
).not.toThrow()
})
})

describe('Foreign Key Management', () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/web/src/lib/erd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,29 @@ export interface Table {
}

const SNAKE_CASE_IDENTIFIER = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/
const SAFE_SQL_TYPE = /^[A-Za-z][A-Za-z0-9_]*(?:\s+[A-Za-z][A-Za-z0-9_]*)*(?:\(\s*\d+(?:\s*,\s*\d+)?\s*\))?(?:\[\])?$/

function assertSnakeCaseIdentifier(kind: string, name: string): void {
if (!SNAKE_CASE_IDENTIFIER.test(name)) {
throw new Error(`${kind} '${name}' must be snake_case.`)
}
}

/**
* Reject SQL type fragments that can escape a column definition.
*
* The accepted grammar intentionally covers common scalar SQL types, optional
* numeric parameters such as `numeric(10,2)`, multi-word types such as
* `timestamp with time zone`, and array suffixes. It rejects statement
* terminators, comments, top-level commas, quotes, operators, and unbalanced
* parentheses so untrusted type text cannot introduce sibling DDL clauses.
*/
function assertValidSqlType(type: string): void {
if (!SAFE_SQL_TYPE.test(type)) {
throw new Error(`Invalid SQL type '${type}'.`)
}
}
Comment thread
seonghobae marked this conversation as resolved.

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

Expand All @@ -49,6 +65,7 @@ export class ERDModel {
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
Loading