← SEARCHABLE ARCHIVE
Web · 2026-07-14

SQL 인젝션은 어떻게 막을까?

Prepared Statement와 최소 권한으로 방어하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

SQL 인젝션은 입력에서 따옴표를 전부 지우는 방식으로 막는 것이 아니다. SQL 명령어와 사용자 데이터를 처음부터 분리해서 데이터가 명령어로 해석되지 않게 만들어야 한다.

Prepared Statement를 사용한다

Prepared Statement(준비된 문장) 또는 Parameterized Query(매개변수화 쿼리)는 SQL 문장에 값이 들어갈 자리를 따로 표시한다.

const sql = 'SELECT id, name, password_hash FROM users WHERE name = ?';
const [rows] = await database.execute(sql, [userName]);

if (rows.length === 1)
{
    const passwordMatches = await passwordHasher.verify(
        rows[0].password_hash,
        password
    );
}

여기서 ?에 들어가는 값은 SQL 문법이 아니라 데이터로 전달된다. 사용자가 따옴표나 OR 같은 문자열을 입력해도 쿼리의 구조를 바꾸지 못한다.

비밀번호는 SQL에서 평문끼리 비교하지 않는다. 사용자 정보를 매개변수 쿼리로 가져온 뒤, 검증된 비밀번호 해시 라이브러리로 저장된 해시와 입력값을 비교한다.

ORM을 사용한다고 자동으로 전부 안전해지는 것은 아니다. ORM이 제공하는 매개변수 기능을 사용하지 않고 Raw Query를 문자열로 합치면 똑같은 취약점이 생긴다.

테이블명과 정렬 기준은 허용 목록으로 검사한다

매개변수는 일반적으로 값에 사용한다. 테이블명, 컬럼명, ASC나 DESC 같은 SQL 문법 자리는 매개변수로 처리할 수 없는 경우가 많다.

이런 값은 사용자의 문자열을 그대로 붙이지 말고 서버가 미리 정한 Allowlist(허용 목록)에서 선택한다.

const allowedSortColumns = {
    newest: 'created_at',
    name: 'name'
};

const sortColumn = allowedSortColumns[requestedSort] ?? 'created_at';
const sql = `SELECT id, name FROM users ORDER BY ${sortColumn} DESC`;

requestedSort 자체를 SQL에 넣지 않고, 서버가 소유한 안전한 문자열로 변환하는 것이 중요하다.

데이터베이스 계정 권한을 줄인다

웹 서버가 사용하는 데이터베이스 계정에 관리자 권한을 주면 취약점 하나의 피해가 데이터베이스 전체로 번질 수 있다.

  • 조회만 하는 기능에는 읽기 권한만 준다.
  • 필요한 테이블에만 접근하게 한다.
  • 웹 애플리케이션 계정으로 데이터베이스 관리 작업을 하지 않는다.
  • 비밀번호는 평문으로 저장하지 않고 검증된 해시 방식으로 저장한다.

오류는 서버에 기록하고 사용자에게 숨긴다

상세한 SQL 오류를 그대로 응답하면 테이블명이나 컬럼명 같은 내부 구조가 노출될 수 있다. 사용자에게는 일반적인 실패 메시지를 보여주고, 자세한 오류는 접근이 통제된 서버 로그에 남겨야 한다.

입력 검증, 방화벽, 탐지 도구도 도움이 된다. 하지만 가장 먼저 해야 할 일은 모든 쿼리를 매개변수화하고, 동적으로 들어가는 SQL 문법은 허용 목록으로 제한하고, 데이터베이스 권한을 최소화하는 것이다.