1998 년부터 OWASP Top 10 단골. 그런데 25 년 지나도 새 시스템에서 발견. 이유 — string concatenation 의 유혹이 너무 자연스러움. 이 가이드는 SQL injection 의 3 type (UNION / blind / time-based), prepared statement 가 wire level 에서 실제 어떻게 막는지, ORM 도 완전 안전 아닌 이유를 정리한다.
Classic Injection — UNION-based
// 취약 코드
const sql = "SELECT * FROM users WHERE id = " + userInput;
db.query(sql);
input: 1
SQL: SELECT * FROM users WHERE id = 1 ← OK
input: 1 OR 1=1
SQL: SELECT * FROM users WHERE id = 1 OR 1=1 ← 모든 user!
input: 1 UNION SELECT username, password FROM admin
SQL: SELECT * FROM users WHERE id = 1 UNION SELECT username, password FROM admin
← admin 의 데이터를 user 데이터인 척 반환
input: 1; DROP TABLE users; --
SQL: SELECT * FROM users WHERE id = 1; DROP TABLE users; --
← DB 파괴 (multi-statement 허용 시)Blind Injection — error 안 보여도 가능
현대 application 은 SQL error 를 사용자에 안 보임.
그래도 가능 — true/false 응답만으로 데이터 한 비트씩 추출.
// 취약 코드
const sql = "SELECT * FROM users WHERE id = " + userInput;
const found = db.query(sql).length > 0;
return found ? "exists" : "not found";
공격:
input: 1 AND substring((SELECT password FROM admin), 1, 1) = 'a'
→ admin 의 password 첫 글자가 'a' 면 "exists" 반환
자동화:
for char in [a, b, c, ..., z, 0-9]:
if response == "exists": 첫 글자 = char
→ 그 다음 글자, 그 다음 ... 전체 password 추출 (수분 내)Time-based Blind — 응답 X, 응답 시간만
true/false 응답조차 동일 (둘 다 "OK").
그래도 공격 가능 — 응답 시간으로 한 비트씩.
input: 1; IF (SELECT password FROM admin WHERE id=1) LIKE 'a%' WAITFOR DELAY '0:0:5'
→ 응답이 5초 걸리면 → 첫 글자 'a'
→ 즉시 응답 → 'a' 아님
자동화 가능. application 의 모든 응답이 "200 OK" 여도 추출 가능.Second-Order Injection — 저장된 데이터로
악성 input 이 처음에 저장만 (실행 X). 나중에 다른 query 에서 사용 → 실행.
Step 1: 회원가입
username = "admin' --"
→ DB 에 그대로 저장 (단순 INSERT, escape 됐다고 가정)
Step 2: 사용자가 자기 정보 수정 (이름 → SQL 에 다시 들어감)
sql = "UPDATE users SET active=1 WHERE name='" + name + "'"
→ "UPDATE users SET active=1 WHERE name='admin' --'"
↑↑↑↑↑
주석 처리, 모두 영향
→ 첫 escape 가 OK 였어도, 저장 후 재사용 시점에 다시 escape 없으면 폭발.Prepared Statement — wire level 에서 어떻게 막는가
// 안전 코드
const sql = "SELECT * FROM users WHERE id = ?";
db.query(sql, [userInput]);
→ wire protocol 에서 2 메시지로 분리:
1. Prepare:
Server <- "PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?'"
Server -> "Statement ID: 42, expects 1 parameter"
2. Execute:
Server <- "EXECUTE 42 WITH params: [user_input_as_bytes]"
Server -> result rows
핵심:
- 1단계의 SQL string 에 user input 없음 — 미리 parsed
- 2단계의 parameter 는 별도 binary slot 으로 전달
- DB 가 그 byte 들을 절대 SQL 로 parse 안 함 → 그냥 value
→ user input 이 무엇이든 (1 OR 1=1, ', DROP TABLE 등) "value" 로 처리.
SQL injection 구조적으로 불가능.ORM 도 완전 안전 아니다
// Sequelize (Node.js) - 안전
User.findOne({where: {id: userInput}}); // ✓ parameterized
// Sequelize - 위험
const sql = "SELECT * FROM users WHERE id = " + userInput;
sequelize.query(sql); // ❌ raw query, 직접 inject 위험
// Active Record (Rails) - 안전
User.where(name: params[:name]) // ✓
// Active Record - 위험
User.where("name = '#{params[:name]}'") // ❌ string interp
User.find_by_sql("SELECT * WHERE id = #{params[:id]}") // ❌
→ ORM 의 raw query / native SQL 옵션은 escape hatch — 사용 시 manual check.
JS template literal (`SELECT * WHERE id = ${id}`) 도 같은 함정.NoSQL injection — 같은 원리 다른 syntax
MongoDB:
db.users.find({username: req.body.username, password: req.body.password});
공격:
POST {username: "admin", password: {"$ne": null}}
→ password = "$ne null" 의미 → "null 이 아닌 어떤 값" 매칭 → 로그인 통과
해결:
- input type validate (string 만 허용, object 거부)
- ORM/ODM 의 typed query (Mongoose schema)
- mongo-sanitize 같은 라이브러리관련 도구
- SQL 포매터 — SQL 의 정확한 구조 확인 (parser 가 어떻게 해석하는지)
흔한 함정
- escape 함수 직접 작성 — quote escape 만으로 못 막음 (UNION, comment, multi-statement 등). prepared statement 만이 구조적 방어.
- integer 라 escape 불필요 가정 — "id 는 숫자 니까 안전" — input 이 실제 문자열이면 폭발. 항상 parameterize.
- ORM 의 raw query 사용 — 위 예시. ORM 신뢰가 모든 query 에 transitive X.
- second-order 무시 — 첫 escape 만 보고 안전 가정. 어디서든 user data 가 SQL 에 들어가면 parameterize.
- DB user 의 권한 과다 — application user 가 DROP TABLE 권한 가지면 SQL injection 시 catastrophic. principle of least privilege (read user / write user 분리).
마무리
SQL injection 은 1998 년부터 알려진 공격이지만 매년 새 시스템에서 발견 — string concatenation 의 유혹이 너무 자연스러움. 방어는 단 하나: 모든 user data 를 prepared statement parameter 로.
실용 — ORM 사용 시 default API 만 (raw query 회피), template literal 로 SQL 짓지 마라, DB user 권한 최소화, application 에서는 SQL error 메시지 노출 X (정보 leak).