← 홈페이지 5강 목록으로
💉
EPISODE 03
Prepared Statement · ORM · 화이트리스트 · 에러 메시지

SQL 인젝션 방어

SQL 인젝션 공격 원리와 대표 페이로드, Prepared Statement(파라미터화 쿼리)가 안전한 이유, ORM 방어 구조, 동적 컬럼명용 화이트리스트, 그리고 에러 메시지 노출 방지를 익힙니다.

SQL injectionPrepared StatementORM
소요 시간
45분
난이도
📊 중급
선수 조건
🎯 sec-02
결과물
절대 SQL 인젝션이 일어날 수 없는 코드 작성 습관

이 강의에서 배우는 것

  • 1SQL 인젝션의 동작 원리를 안다
  • 2Prepared Statement (?, $1, @name)을 사용한다
  • 3ORM이 자동으로 파라미터화하는 이유를 안다
  • 4동적 컬럼명/정렬 방향에 화이트리스트를 적용한다
  • 5에러 메시지를 사용자에게 노출하지 않는다

1. SQL 인젝션이란

javascript
// 취약: 사용자 입력을 문자열로 직접 삽입
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;

공격자 입력: username = admin' --, password = 아무거나

sql
SELECT * FROM users WHERE username = 'admin' --' AND password = '아무거나'
-- 이후가 주석 처리 → 비밀번호 확인 없이 admin 로그인!

2. 대표 페이로드

sql
-- 항상 참
' OR '1'='1
' OR 1=1 --

-- 주석으로 무력화
admin' --
admin'/*

-- UNION으로 다른 쿼리
' UNION SELECT username, password FROM users --

-- 데이터 삭제
'; DROP TABLE users; --

3. Prepared Statement (파라미터화 쿼리)

javascript
// ✗ 취약
const query = `SELECT * FROM users WHERE username = '${username}'`;
db.query(query);

// ✓ 안전
const stmt = db.prepare('SELECT * FROM users WHERE username = ? AND password = ?');
const user = stmt.get(username, password);
// username에 ' OR 1=1 --  를 넣어도 SQL로 해석되지 않고 그냥 문자열로 처리

원리: 일반 쿼리는 SQL 파싱 → 데이터 삽입 → 실행. Prepared는 SQL 파싱 → 실행 → 데이터 바인딩. 데이터가 SQL 파싱에 개입할 수 없음.

4. ORM은 자동 방어

javascript
// Prisma — 자동으로 Prepared Statement
const user = await prisma.user.findFirst({
  where: { username: username }
});
// 내부: SELECT * FROM users WHERE username = $1
// username 값은 항상 데이터로 처리됨

// Mongoose — NoSQL이라 SQL 인젝션 없음 (NoSQL 인젝션 별도 주의)
const user = await User.findOne({ username: username });

5. 동적 컬럼명 — 화이트리스트

javascript
// 컬럼명/테이블명은 Prepared Statement로 해결 불가
const ALLOWED_SORT_COLUMNS = ['name', 'price', 'created_at'];
const ALLOWED_ORDERS = ['ASC', 'DESC'];

function buildSafeQuery(sortBy, order) {
  const safeColumn = ALLOWED_SORT_COLUMNS.includes(sortBy) ? sortBy : 'created_at';
  const safeOrder = ALLOWED_ORDERS.includes(order?.toUpperCase()) ? order.toUpperCase() : 'DESC';

  return `SELECT * FROM products ORDER BY ${safeColumn} ${safeOrder}`;
}

6. 에러 메시지 노출 금지

javascript
// ✗ 나쁜 예 — DB 구조 노출
catch (err) {
  res.status(500).json({ error: err.message });
  // "SQLITE_ERROR: no such table: users" → 테이블 이름 노출!
}

// ✓ 좋은 예
catch (err) {
  console.error('[에러]', err);  // 서버 로그
  res.status(500).json({ error: '검색 중 오류가 발생했습니다.' });
}
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗