Skip to content

03. SQL 인젝션과 파라미터화된 쿼리

SQL 인젝션의 핵심은 데이터가 쿼리 구조로 해석되는 순간이다. 따라서 근본 방어는 문자열 이스케이프가 아니라 쿼리 구조와 데이터 값을 분리하는 것이다.

학습 목표

  1. SQL 인젝션이 왜 발생하는지 구조적으로 설명할 수 있다.
  2. 파라미터화된 쿼리와 ORM 바인딩이 왜 근본 대책인지 이해한다.
  3. ORDER BY, 컬럼명, 테이블명처럼 플레이스홀더를 쓸 수 없는 영역의 방어 원칙을 설명할 수 있다.
  4. 이스케이프 중심 대응의 한계를 이해한다.

1. SQL 인젝션은 어떻게 생길까?

문제는 SQL 자체가 아니라, 애플리케이션이 사용자 입력을 쿼리 문법 일부처럼 취급하는 데 있다.

대표적인 위험 상황은 다음과 같다.

  • 문자열 덧붙이기로 WHERE 절 구성
  • 정렬 키를 그대로 ORDER BY에 삽입
  • 검색어를 Raw SQL 템플릿에 직접 넣음
  • ORM을 쓰면서 특정 구간만 문자열 SQL로 우회

2. 근본 대책은 파라미터화된 쿼리다

파라미터화된 쿼리는 쿼리 구조와 값을 분리한다.

  • SQL 문장은 미리 고정한다
  • 사용자 입력은 바인딩 값으로만 전달한다
  • DB는 전달된 값을 문법이 아니라 데이터로 처리한다

PHP PDO 예시

php
$stmt = $pdo->prepare(
  'SELECT id, title, created_at FROM posts WHERE author_id = :authorId AND status = :status'
);

$stmt->execute([
  ':authorId' => $authorId,
  ':status' => 'published',
]);

$rows = $stmt->fetchAll();

Node.js (PostgreSQL pg) 예시

ts
const rows = await db.query(
  'SELECT id, title, created_at FROM posts WHERE author_id = $1 AND status = $2',
  [authorId, 'published'],
);

드라이버마다 플레이스홀더 문법은 다를 수 있다.

  • PostgreSQL pg: $1, $2
  • MySQL 계열 드라이버: ?
  • ORM: 라이브러리별 바인딩 문법 사용

문법은 달라도 원칙은 같다. 문자열 결합이 아니라 바인딩 값 전달이 핵심이다.

이 방식이 기본값이어야 한다.
원문처럼 이스케이프 함수 설명을 중심에 두는 방식은 현재 기준으로 보조 수단에 가깝다.


3. ORM을 써도 안전이 자동 완성되지는 않는다

ORM은 기본적으로 안전한 방향으로 설계되어 있지만, 아래처럼 우회하면 다시 위험해진다.

  • Raw query를 직접 작성
  • 조건절 일부를 문자열로 조립
  • 동적 정렬 키를 검증 없이 그대로 전달

즉, ORM은 도움을 주지만 Raw SQL을 허용하는 순간 개발자가 다시 경계를 책임져야 한다.


4. 플레이스홀더를 쓸 수 없는 영역

일반적으로 값 자리에는 플레이스홀더를 쓸 수 있지만, SQL 식별자에는 바로 쓸 수 없다.

대표 예시는 다음과 같다.

  • 컬럼명
  • 테이블명
  • 정렬 방향
  • 일부 함수명

이 경우 원칙은 하나다.

Allow-list로 고정한다

ts
const sortMap = {
  newest: 'created_at DESC',
  oldest: 'created_at ASC',
  popular: 'view_count DESC',
} as const;

const sortClause = sortMap[sortKey] ?? sortMap.newest;

사용자가 보내는 값을 그대로 붙이지 말고, 미리 정의한 안전한 구문 중 하나를 선택하게 해야 한다.


5. 검색 기능에서 자주 놓치는 점

검색어가 LIKE에 들어가는 경우도 많다.
이때도 원칙은 같다.

  • 와일드카드를 붙일지 말지는 애플리케이션이 결정한다
  • 검색어 자체는 바인딩 값으로 넣는다
  • 필요한 경우 %, _ 같은 와일드카드 처리 정책을 따로 둔다

중요한 점은 검색 기능이라도 문자열 결합 예외를 만들지 않는 것이다.


6. 이스케이프 함수는 왜 부족할까?

원문에는 오래된 PHP 관점에서 이스케이프 설명이 길게 나온다.
그 맥락은 이해할 수 있지만, 현재 기준에서 한계가 명확하다.

방식한계
문자열 이스케이프DB 설정, 문자셋, 쿼리 구조에 따라 실수 여지가 남음
전역 자동 이스케이프 기대오래된 기능 의존, 예측 어려움
입력 치환 후 문자열 결합특정 구간이 빠지는 순간 다시 취약

따라서 지금은 이렇게 정리하는 편이 정확하다.

  • 기본 대책: 파라미터화된 쿼리
  • 예외 구간: Allow-list 식별자 선택
  • 추가 대책: 최소 권한 DB 계정, 감사 로그, 에러 메시지 제어

7. DB 권한도 방어층이다

SQL 인젝션이 생겨도 피해를 줄이려면 DB 권한이 좁아야 한다.

  • 애플리케이션 계정은 필요한 스키마만 접근
  • 관리자 권한 계정으로 웹 앱을 구동하지 않음
  • 파일 시스템 접근 권한, 위험한 확장 기능은 기본 비활성화
  • 운영 비밀값은 코드에 하드코딩하지 않고 비밀 저장소나 환경변수로 관리

원문처럼 애플리케이션 코드 안에 DB 접속 정보를 직접 두는 예시는 현재 기준으로는 안티패턴 예시로 보는 편이 맞다.


8. 코드 리뷰에서 자주 보는 위험 신호

  • SELECT ... " + userInput + ...
  • ORDER BY ${sortKey}
  • ORM 사용 중 raw, unsafe, literal 같은 API 남용
  • 에러 메시지에 SQL 문장이나 DB 예외를 그대로 출력
  • DB 접속 계정이 지나치게 강한 권한을 가짐

9. PR 리뷰 체크리스트

  • 값이 들어가는 모든 쿼리가 파라미터 바인딩을 쓰는가
  • 컬럼명, 정렬 키, 방향은 Allow-list로 제한되는가
  • Raw SQL 사용 이유가 분명하고 리뷰되었는가
  • 애플리케이션 계정에 최소 권한이 적용되어 있는가
  • DB 오류가 사용자에게 그대로 노출되지 않는가
  • 비밀값이 코드나 저장소에 하드코딩되지 않는가

핵심 정리

  • SQL 인젝션은 입력값이 SQL 문법으로 해석될 때 발생한다
  • 근본 대책은 파라미터화된 쿼리와 ORM 바인딩이다
  • 플레이스홀더를 쓸 수 없는 식별자 영역은 Allow-list로 고정해야 한다
  • 이스케이프 함수는 보조 수단일 수는 있어도 기본 방어 전략이 되어서는 안 된다