SQL Injection은 사용자 입력을 SQL 쿼리 문자열에 직접 연결할 때 발생한다. 공격자는 입력값에 SQL 문법을 넣어 쿼리 의미를 바꾼다.
SQL Injection 방어 실전 가이드
SQL Injection 공격 원리부터 방어 전략을 총정리한다. Prepared Statements, ORM 활용, 입력 검증, WAF 설정과 실제 공격 사례 분석 및 자동 스캔 도구 활용법을 포함한다.
한 줄 요약: SQL Injection은 사용자 입력이 SQL 쿼리의 일부로 해석될 때 발생한다. Prepared Statement를 쓰면 원천 차단된다.
SQL Injection은 1990년대부터 알려진 공격이지만 2026년 현재도 OWASP Top 10에 포함될 만큼 여전히 실제로 발생한다. 이유는 단순하다 — 구현이 쉽고, 한 줄 실수로 전체 데이터베이스가 노출된다. 이 글에서는 공격 원리를 정확히 이해하고, 코드 레벨에서 방어하는 방법을 예시와 함께 정리한다.
공격 원리 — 입력이 코드가 되는 순간

SQL Injection 공격 시나리오// 취약한 로그인 쿼리 const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`; // 공격자가 입력하는 값: // email: admin@example.com' -- // password: anything // 완성되는 쿼리 (-- 이후는 주석 처리됨): // SELECT * FROM users WHERE email = 'admin@example.com' --' AND password = 'anything' // 결과: password 검증 없이 admin 계정으로 로그인 성공 // 더 위험한 예: // email: ' OR '1'='1 // 완성 쿼리: SELECT * FROM users WHERE email = '' OR '1'='1' ... // 결과: 모든 사용자 레코드 반환
SQL Injection의 주요 유형
- Classic SQLi: 에러 메시지나 응답 차이로 DB 정보를 직접 추출. 가장 기본적인 형태.
- Blind SQLi: 응답에 데이터가 직접 나오지 않지만 참/거짓 응답 차이로 정보를 유추. 더 느리지만 조용하다.
- Time-based Blind SQLi: DB에
SLEEP(5)같은 지연 함수를 주입해 응답 시간으로 정보 추출. - Out-of-band SQLi: DNS나 HTTP 요청을 통해 외부 서버로 데이터 전송. 방화벽 우회에 사용.
- Second-order SQLi: 입력값이 즉시 실행되지 않고, 나중에 다른 쿼리에서 사용될 때 발생. 가장 탐지하기 어려운 유형.
Prepared Statements — 원천 차단의 표준
Prepared Statement(준비된 구문)는 쿼리 구조를 먼저 컴파일하고, 사용자 입력을 별도의 파라미터로 바인딩한다. 입력값이 SQL 문법으로 해석되지 않기 때문에 SQL Injection이 원천적으로 불가능하다.

취약한 코드 vs 안전한 코드 — Node.js + MySQLconst mysql = require('mysql2/promise'); // 취약한 코드 — 문자열 직접 연결 async function findUserVulnerable(email, password) { const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`; const [rows] = await connection.query(query); return rows[0]; } // 안전한 코드 — Prepared Statement (파라미터 바인딩) async function findUserSafe(email, password) { const query = 'SELECT * FROM users WHERE email = ? AND password_hash = ?'; const hashedPassword = await bcrypt.hash(password, 10); const [rows] = await connection.execute(query, [email, hashedPassword]); // connection.execute()는 내부적으로 Prepared Statement 사용 return rows[0]; } // PostgreSQL (pg 패키지) — 동일 원리 async function findUserPostgres(email) { const result = await pool.query( 'SELECT id, name, email FROM users WHERE email = $1', [email] // $1에 바인딩, 절대 쿼리 문자열로 해석 안 됨 ); return result.rows[0]; }
const allowedColumns = ['name', 'email', 'created_at']; if (!allowedColumns.includes(sortBy)) throw new Error('Invalid column');ORM 활용 — Prisma, TypeORM, Sequelize
현대 Node.js 프로젝트에서 ORM을 쓰면 Prepared Statement가 자동으로 처리된다. 하지만 ORM에서도 raw 쿼리를 쓰거나, 잘못된 패턴을 사용하면 취약점이 생긴다.
Prisma — 안전한 사용 vs 위험한 raw 쿼리// 안전 — Prisma ORM API 사용 (자동으로 파라미터 바인딩) const user = await prisma.user.findFirst({ where: { email: userInputEmail, // 자동으로 파라미터화됨 isActive: true }, select: { id: true, name: true, email: true } // 필요한 필드만 선택 }); // 안전 — Prisma $queryRaw (템플릿 리터럴로 자동 이스케이프) const result = await prisma.$queryRaw` SELECT * FROM users WHERE email = ${userInputEmail} `; // 주의: 일반 문자열이 아닌 태그드 템플릿 리터럴로 사용해야 함 // 위험 — 문자열 보간 사용 (절대 금지) const badResult = await prisma.$queryRawUnsafe( `SELECT * FROM users WHERE email = '${userInputEmail}'` // SQLi 취약 ); // TypeORM — 안전한 파라미터 바인딩 const user = await userRepository .createQueryBuilder('user') .where('user.email = :email', { email: userInputEmail }) .getOne();
입력 검증과 WAF — 다층 방어
Prepared Statement로 SQLi를 원천 차단하더라도, 입력 검증과 WAF를 추가하면 방어 깊이가 높아진다. 특히 레거시 코드가 혼재하는 환경에서 추가 방어선이 된다.
입력 검증 레이어
- 이메일 필드에 SQL 예약어나 특수문자가 포함되면 애초에 거부
- 숫자 파라미터는 반드시 정수/숫자로 캐스팅 후 검증
- 긴 입력값은 DB 컬럼 길이를 초과하기 전에 거부 — Buffer Overflow 방지도 됨
WAF(Web Application Firewall)
WAF는 알려진 SQL Injection 패턴을 HTTP 레벨에서 탐지·차단한다. Cloudflare WAF, AWS WAF, ModSecurity(오픈소스)가 대표적이다. WAF는 알려진 공격만 막으므로 단독 방어선으로 쓰면 안 되고, Prepared Statement와 함께 사용해야 한다.

최소 권한 원칙 — DB 계정 권한 분리-- 애플리케이션 전용 DB 계정 생성 (최소 권한) CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password'; -- 필요한 권한만 부여 (DROP, CREATE, ALTER 등 DDL 권한 없음) GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_user'@'%'; -- 관리 작업용 별도 계정 (마이그레이션 등에만 사용) CREATE USER 'app_admin'@'localhost' IDENTIFIED BY 'admin_password'; GRANT ALL PRIVILEGES ON myapp.* TO 'app_admin'@'localhost'; -- 애플리케이션 .env -- DB_USER=app_user ← 제한된 권한만 가진 계정 사용 -- 공격자가 SQLi로 쿼리를 실행해도 DROP TABLE 등은 불가능
Internal server error)로 변환하고, 상세 에러는 서버 로그에만 기록한다.탐지와 테스트 — 취약점을 직접 찾는 방법
방어 코드를 작성했다면 실제로 취약점이 없는지 테스트해야 한다. 자동화 도구와 수동 테스트를 병행한다.
자동화 도구
- sqlmap: 오픈소스 SQLi 자동 탐지 도구. CI/CD 파이프라인에 통합 가능.
sqlmap -u "https://example.com/api?id=1" --batch - OWASP ZAP: 웹 앱 전반 취약점 스캔. SQLi 탐지 모듈 포함.
- Semgrep: 정적 분석으로 코드에서 취약한 쿼리 패턴 탐지. CI에 통합해 PR 단계에서 차단.
수동 테스트 — 기본 페이로드
입력 필드에 다음 페이로드를 입력하고 에러 또는 비정상 응답이 오는지 확인한다. 비정상 응답이 있으면 즉시 취약점 분석이 필요하다.
'— 단일 따옴표로 SQL 구문 에러 유발1 OR 1=1— 항상 참인 조건1' AND SLEEP(5) --— 5초 지연이 발생하면 Time-based SQLi 취약