이 글의 목차

SQL Injection(SQLi)의 원리와 방어

주의 사항

  1. 교육 목적으로만 이용해 주세요.
  2. 무단 침입, 데이터 유출, 개인 정보 침해 등 불법적인 활동은 심각한 법적 결과를 초래할 수 있습니다.
  3. 개인적인 테스트 환경을 구축해서 실습하시길 바랍니다.

1. 사전 지식: 웹 서비스의 3단계 데이터 흐름 (Web - WAS - DB)

SQL Injection이 발생하는 원리를 정확히 이해하려면, 먼저 우리가 웹사이트에서 버튼을 누르고 로그인할 때 뒷단에서 어떤 시스템들이 움직이는지 알아야 합니다. 우리가 사용하는 대부분의 웹 서비스는 크게 3가지 단계를 거쳐 데이터를 처리합니다.

[사용자 브라우저]
       │ (1) ID/PW 입력 후 로그인 버튼 클릭
       ▼
[1. 웹 서버 (Web Server)]
       │ (2) "로그인 요청 들어왔어. 연산이 필요하니 넘겨줄게."
       ▼
[2. WAS (Web Application Server)]
       │ (3) "DB에 이 사용자가 있는지 조회하는 SQL 명령을 보낼게." (SQL 전달)
       ▼
[3. 데이터베이스 (Database, DB)]
         - SQL 명령을 실행하여 회원 정보를 찾아 WAS에 돌려줌

① 웹 서버 (Web Server)

역할: 사용자의 화면(브라우저)과 가장 먼저 만나는 관문입니다. 변하지 않는 고정된 파일(HTML, CSS, 이미지 등)을 사용자에게 그대로 보여주는 역할을 합니다.

특징: 스스로 어떠한 논리적인 계산이나 데이터 검색을 수행하지 못합니다. 사용자가 “로그인해 줘"라고 요청하면, 이 요청을 다음 단계인 WAS로 그대로 넘겨줍니다.

② WAS (Web Application Server)

역할: 로그인 성공 여부 판단, 회원가입 처리, 결제 금액 계산 등 실제 프로그램의 핵심 비즈니스 로직(연산)을 처리하는 곳입니다.

특징: 사용자가 입력한 아이디와 비밀번호가 올바른지 확인하기 위해, 데이터를 보관하고 있는 데이터베이스와 끊임없이 소통합니다. 이때 데이터베이스와 대화하기 위한 ‘명령문’을 작성하는 주체가 바로 이 WAS입니다.

③ 데이터베이스 (Database, DB)

역할: 회원의 아이디, 비밀번호, 게시글, 결제 내역 등 웹 서비스에 존재하는 모든 데이터를 안전하게 저장하고 관리하는 시스템입니다.

특징: 스스로 움직이지 않으며, 오직 WAS가 보낸 명령어(SQL)에 적힌 대로만 데이터를 찾아서 전달하거나 저장합니다.


2. SQL이란 무엇인가요?

**SQL(Structured Query Language)**은 데이터베이스라는 컴퓨터 시스템이 알아들을 수 있도록 구조화된 전용 ‘질의 언어(명령문)‘입니다.

WAS가 데이터베이스에게 데이터를 요청할 때는 일반적인 한국어나 영어 문장이 아닌, 반드시 SQL이라는 문법에 맞춰서 명령해야 합니다.

정상적인 SQL 명령의 예:

SELECT * FROM users WHERE username = 'hong123';

해석: “users 테이블에서 username이 ‘hong123’인 사용자의 정보를 전부 찾아서 보여줘.”


3. SQL Injection의 발생 원리와 작동 논리

SQL Injection(SQL 주입 공격)은 사용자가 입력값을 넣는 곳에 의도적으로 조작된 SQL 문법(명령어)을 교묘하게 섞어 넣음으로써, 시스템이 이를 단순한 데이터가 아닌 ‘실행해야 할 명령문’으로 착각하게 만드는 공격 기법입니다.

이 취약점이 발생하는 핵심 원리는 **“데이터와 명령어가 제대로 구분되지 않기 때문”**입니다.

컴퓨터가 사용자의 입력값에 속는 과정

웹 시스템이 로그인 요청을 처리할 때, WAS는 다음과 같은 구조로 SQL 명령문을 조립합니다.

-- 원래 정상적인 로그인 검증 쿼리 구조
SELECT * FROM users WHERE username = '사용자입력ID' AND password = '사용자입력PW';

만약 보안 처리가 되어 있지 않은 시스템이라면, 컴퓨터는 사용자입력ID 자리에 들어오는 글자를 아무런 의심 없이 그대로 붙여서 하나의 거대한 명령어로 인식합니다.

1) 정상적인 상황

  • 사용자 입력: ID에 admin, PW에 password123 입력

조립된 최종 SQL:

SELECT * FROM users WHERE username = 'admin' AND password = 'password123';

결과: 데이터베이스는 ID가 admin이고 비밀번호가 password123인 회원이 있는지 정확히 비교합니다. 일치하는 데이터가 있으면 로그인이 성공합니다.

2) 공격 상황 (SQL Injection)

공격자는 비밀번호를 알아낼 필요가 없습니다. 대신 비밀번호 입력칸에 문법적인 특수문자와 항상 참이 되는 논리식을 입력합니다.

  • 사용자 입력: ID에 admin, PW에 ' OR '1'='1 입력

조립된 최종 SQL:

SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1';

이때 데이터베이스 내부의 연산 순서에 따라 이 문장은 아래와 같이 강제로 구조가 조작됩니다.

  1. 따옴표(')의 조기 종료: 비밀번호 값의 시작을 알리는 따옴표가 공격자가 입력한 따옴표 '에 의해 즉시 닫혀버립니다. (password = '')
  2. 논리 연산의 우선순위: 컴퓨터는 AND 연산을 OR 연산보다 먼저 처리합니다. 따라서 앞부분인 (username = 'admin' AND password = '')가 먼저 계산되어 거짓(False)이 됩니다.
  3. 항상 참인 조건의 실행: 그 뒤에 남은 OR '1'='1' 연산이 수행됩니다. 컴퓨터에게 '1'='1'은 언제나 참(True)입니다.
  4. 전체 결과의 참(True)화: 앞의 아이디/비밀번호 대조 결과가 거짓이었더라도, 뒤의 OR 참(True) 조건 때문에 전체 문장(WHERE 절)의 결과가 무조건 참(True)이 되어버립니다.

결국 데이터베이스는 비밀번호가 일치하지 않음에도 불구하고 문장 전체가 참이라는 판단 하에 admin 계정의 정보를 WAS로 넘겨주고, 공격자는 비밀번호 없이 관리자 권한으로 로그인에 성공하게 됩니다.


4. SQL Injection을 완벽하게 방어하는 기술적 해결책

이 치명적인 보안 문제를 예방하기 위한 핵심 원칙은 **“사용자의 입력값이 어떤 경우에도 명령어로 해석되지 않고, 순수한 데이터(글자 그대로)로만 취급되도록 강제하는 것”**입니다.

① Prepared Statement (파라미터화된 쿼리) 사용

가장 확실하고 표준적인 방어 대책입니다.

개발자가 SQL 문을 작성할 때, 사용자의 입력값이 들어갈 자리를 비워두고 쿼리의 기본 뼈대(구조)를 먼저 데이터베이스에 보내 컴파일(해석)을 끝마쳐 둡니다.

-- 1단계: 뼈대 컴파일 (컴퓨터는 이 구조를 명령어로 확정함)
SELECT * FROM users WHERE username = ? AND password = ?;

-- 2단계: 빈자리(?)에 사용자의 입력값을 단순 텍스트 데이터로 끼워 넣음

이렇게 하면 사용자가 아무리 ' OR '1'='1 같은 특수 기호를 입력해도, 이미 명령 구조가 확정된 상태이기 때문에 데이터베이스는 이를 명령어의 일부로 해석하지 않습니다. 오직 “사용자 비밀번호가 아주 길고 이상한 ' OR '1'='1이라는 문자열이구나"라고만 인식하여 단순 비교를 수행하므로 공격이 무력화됩니다.

② 안전한 ORM(Object-Relational Mapping) 프레임워크 사용 및 주의점

Spring 프레임워크의 JPA나 Hibernate 같은 현대적인 ORM 도구를 사용하면, 기본적으로 내부에서 Prepared Statement 방식을 사용하여 SQL Injection을 자동으로 예방해 줍니다.

다만, SQL을 직접 작성하는 MyBatis 등의 도구를 사용할 때는 문법에 주의해야 합니다.

취약한 방식 (${변수}):

<!-- 이 방식은 사용자가 입력한 값을 SQL 문장에 그대로 갖다 붙이므로 취약합니다 -->
SELECT * FROM users WHERE username = '${username}'

안전한 방식 (#{변수}):

<!-- 이 방식은 자동으로 Prepared Statement(파라미터화)로 처리되므로 안전합니다 -->
SELECT * FROM users WHERE username = #{username}

③ 입력값 검증 및 필터링 (Input Validation)

웹 브라우저와 서버 양측에서 사용자가 입력한 값에 비정상적인 특수문자나 SQL 예약어(SELECT, UNION, DROP, --, # 등)가 들어있는지 검사하고, 발견 시 요청을 차단하는 보안 필터를 적용하는 방법입니다. 다만, 이는 Prepared Statement를 보조하는 2차 방어선으로 사용하는 것이 안전합니다.