목차
- 1. 왜 아직도 SQL 인젝션이 계속 나올까요?
- 2. 개념부터 짚고 가보겠습니다
- 2-1. 취약한 쿼리는 어떻게 만들어질까요?
- 2-2. SQL 인젝션 방어의 핵심은 뭘까요?
- 3. 모의 해킹 사례: 로그인 우회 취약점 재현
- 3-1. 테스트 환경
- 3-2. 1단계: 요청 패턴 확인
- 3-3. 2단계: 수동 페이로드로 반응 확인
- 3-4. 3단계: 로그인 우회 확인
- 4. 취약한 코드와 안전한 코드 비교
- 4-1. 문자열 결합 방식의 문제
- 4-2. Prepared Statement로 수정
- 4-3. 정렬 조건은 따로 처리해야 합니다
- 5. SQL 인젝션 방어 전략을 운영 관점에서 정리해보겠습니다
- 5-1. 코드 레벨 방어
- 5-2. DB 레벨 방어
- 5-3. 인프라 레벨 방어
- 5-4. 어떤 방어가 어디까지 막아주나요?
- 6. ⚠️ 실제로 많이 겪는 문제와 트러블슈팅
- 6-1. 이스케이프만 하면 된다고 믿는 경우
- 6-2. ORM 쓰니까 안전하다고 생각하는 경우
- 6-3. 에러 페이지가 너무 친절한 경우
- 6-4. 탐지 룰이 없어서 시도 자체를 모르는 경우
- 7. 검증: 수정 후 어떻게 확인할까요?
- 7-1. 간단 체크리스트
- 8. 정리와 다음 단계
- FAQ
- Q1. WAF만 있으면 SQL 인젝션 방어가 충분한가요?
- Q2. 내부망 서비스도 꼭 점검해야 하나요?
- Q3. 침투 테스트는 어디부터 시작하면 좋을까요?
[보안] SQL 인젝션 방어: 모의 해킹 사례로 본 실전 대응
웹 개발 13년, 인프라 운영 경험으로 느낀 건 SQL 인젝션 방어가 아직도 현장에서 가장 흔한 취약점이라는 점입니다. 특히 급하게 만든 관리자 페이지, 레거시 PHP 코드, 내부 업무용 도구에서 툭 튀어나오더라고요. 대단한 제로데이보다 기본기 부족에서 더 많이 터집니다. 이번 글은 제가 홈랩과 사내 교육 환경에서 직접 재현했던 모의 해킹 사례를 바탕으로, SQL 인젝션이 어떻게 발견되고 어떤 순서로 막아야 하는지 정리한 글입니다. 침투 테스트를 준비하는 분이나 OWASP Top 10을 실무 관점에서 이해하고 싶은 분께 도움이 될 거에요.
처음엔 "요즘 누가 저렇게 단순한 실수를 해?" 싶었는데, 막상 로그를 따라가 보니 사소한 문자열 결합 하나가 인증 우회(Authentication Bypass)로 이어지더라고요. 여기서 중요한 포인트! SQL 인젝션 방어는 웹방화벽 하나 올린다고 끝나는 문제가 아니라, 코드 작성 습관과 검증 절차까지 같이 바뀌어야 합니다.
사용자 입력이 웹 서버, 애플리케이션, 데이터베이스로 흐르면서 어느 지점에서 검증과 바인딩이 필요한지 보여주는 개요 이미지입니다.
1. 왜 아직도 SQL 인젝션이 계속 나올까요?
쉽게 말해 SQL 인젝션(SQL Injection)은 사용자 입력값이 쿼리 문자열에 그대로 섞여 들어가는 문제입니다. 공격자는 입력창에 단순한 아이디나 검색어 대신 SQL 구문 일부를 넣고, 애플리케이션이 그걸 그대로 실행하게 만드는 거죠.
제가 실무에서 자주 봤던 패턴은 이런 식입니다.
- 로그인 페이지에서 아이디와 비밀번호를 문자열로 이어붙임
- 검색 페이지에서 LIKE 조건을 직접 조합함
- 정렬 파라미터(sort, order by)를 화이트리스트 없이 그대로 사용함
- 에러 메시지를 자세히 노출해서 DB 구조를 유추하게 만듦
OWASP Top 10에서도 인젝션 계열 취약점은 계속 중요하게 다뤄집니다. 이름이나 분류는 시기에 따라 조금씩 달라져도, 본질은 같습니다. 신뢰하면 안 되는 입력을 신뢰했다. 결국 여기로 돌아오더라고요.
2. 개념부터 짚고 가보겠습니다
2-1. 취약한 쿼리는 어떻게 만들어질까요?
예를 들어 로그인 로직이 아래처럼 되어 있다고 해보겠습니다. 이런 코드는 교육용 예제에서 정말 많이 보지만, 레거시 시스템에서도 형태만 조금 바뀐 채 남아 있는 경우가 있습니다.
<?php
$id = $_POST['id'];
$pw = $_POST['pw'];
$sql = "SELECT * FROM users WHERE id = '$id' AND pw = '$pw'";
$result = mysqli_query($conn, $sql);
?>
문제는 <code>$id나 $pw에 작은따옴표와 조건식을 넣을 수 있다는 점입니다. 예를 들어 공격자가 아래처럼 넣으면요.
id: admin' --
pw: anything
실제 쿼리는 대략 이런 식으로 바뀝니다.
SELECT * FROM users WHERE id = 'admin' --' AND pw = 'anything'
-- 뒤는 주석 처리되니 비밀번호 검증이 사실상 사라집니다. 처음 보면 "이게 진짜 먹나?" 싶은데, 조건식과 주석 규칙을 이해하면 왜 위험한지 바로 보입니다.
2-2. SQL 인젝션 방어의 핵심은 뭘까요?
제가 여러 번 삽질해보고 정리한 기준은 딱 네 가지입니다.
- Prepared Statement(준비된 문장) 또는 Parameterized Query(매개변수화 쿼리) 사용
- Input Validation(입력 검증)과 Allowlist(허용 목록) 적용
- Least Privilege(최소 권한) 원칙으로 DB 계정 분리
- Logging & Monitoring(로깅과 모니터링)으로 이상 징후 탐지
많은 분이 1번만 기억하시는데, 실제로 운영해보면 3번과 4번이 사고 범위를 줄이는 데 정말 큽니다.
3. 모의 해킹 사례: 로그인 우회 취약점 재현
이 사례는 실제 고객 시스템이 아니라, 제가 홈랩에서 만든 교육용 환경입니다. 민감한 정보 없이 재현 가능한 형태로 구성했고, 침투 테스트 관점에서 어떤 흐름으로 점검하는지 보여드리려고 준비했습니다.
3-1. 테스트 환경
| 구성 요소 | 역할 | 설명 |
|---|---|---|
| 웹 애플리케이션 | 로그인 기능 제공 | 의도적으로 취약한 문자열 결합 방식 사용 |
| MariaDB/MySQL 계열 DB | 사용자 정보 저장 | 일반적인 LAMP 계열 환경과 유사하게 구성 |
| Burp Suite Community | 요청 가로채기 | 프록시(Proxy) 용도로 활용 |
| sqlmap | 자동화 점검 보조 | 수동 확인 후 보조적으로 사용 |
실제로 써보니까 자동화 도구부터 돌리는 것보다, 먼저 요청 구조를 사람이 이해하는 게 훨씬 중요하더라고요. 자동화는 빨리 찾게 해주지만, 원인 분석까지 대신해주진 않습니다.
3-2. 1단계: 요청 패턴 확인
- 브라우저 프록시를 Burp로 설정합니다.
- 로그인 페이지에 임의 계정으로 요청을 보냅니다.
- POST Body에
id,pw가 어떻게 전달되는지 확인합니다. - 응답 코드와 에러 메시지 차이를 비교합니다.
POST /login HTTP/1.1
Host: lab.local
Content-Type: application/x-www-form-urlencoded
id=test&pw=test1234
제가 가장 먼저 보는 건 세 가지입니다. 입력값 필터링이 있는지, 실패 응답이 모두 같은지, 그리고 데이터베이스 에러가 노출되는지요. 이 세 개만 봐도 절반은 감이 옵니다.
3-3. 2단계: 수동 페이로드로 반응 확인
다음으로는 아주 기본적인 문자열부터 넣어봅니다.
'\n"\n' OR '1'='1
만약 작은따옴표 하나만 넣었는데 500 에러가 나거나 SQL 문법 오류가 보이면, 일단 입력값이 쿼리에 직접 반영되고 있을 가능성이 큽니다. 여기서 너무 세게 들어가지 말고, 반응 차이를 보는 수준에서 멈추는 게 좋습니다. 모의 해킹은 증명이 목적이지, 실제 데이터 훼손이 목적이 아니거든요.
로그인 요청의 파라미터 구조와 응답 차이를 분석하는 장면을 보여주는 이미지입니다.
3-4. 3단계: 로그인 우회 확인
교육용 환경에서는 아래 같은 페이로드로 우회가 재현됐습니다.
id=admin' -- \npw=anything
또는 조건식을 분기시키는 방식도 가능합니다.
id=' OR '1'='1' -- \npw=anything
제가 직접 해보니 재미있는 지점이 하나 있었습니다. 개발자는 "비밀번호도 같이 검사하니까 괜찮다"라고 생각했는데, 실제로는 아이디 필드 하나만으로도 뒤쪽 조건이 다 무력화되더라고요. 현장에서 정말 흔한 착각입니다.
4. 취약한 코드와 안전한 코드 비교
4-1. 문자열 결합 방식의 문제
# vulnerable example
username = request.form['username']
password = request.form['password']
query = f"SELECT id FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
이 코드는 보기엔 간단하지만, 입력값에 쿼리 제어권을 넘겨주는 셈입니다. 저도 예전에 사내 도구를 빠르게 붙이다가 비슷한 유혹을 받은 적이 있는데요. "내부망인데 뭐" 하고 넘어가면 나중에 더 크게 돌아옵니다. 내부 시스템도 피싱이나 계정 탈취 이후엔 공격 표적이 되거든요.
4-2. Prepared Statement로 수정
# safer example
username = request.form['username']
password = request.form['password']
query = "SELECT id FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))
핵심은 DB 드라이버가 데이터와 쿼리 구조를 분리해서 처리하게 만드는 겁니다. 즉, 사용자 입력은 더 이상 SQL 구문이 아니라 단순 값으로만 취급됩니다. SQL 인젝션 방어의 가장 기본이자 가장 강력한 방법이죠.
4-3. 정렬 조건은 따로 처리해야 합니다
여기서 많이 놓치는 부분이 있습니다. 테이블명, 컬럼명, 정렬 방향 같은 건 파라미터 바인딩으로 막기 어려운 경우가 있거든요. 이런 값은 반드시 허용 목록으로 제한해야 합니다.
allowed_sort = {"created_at", "username", "last_login"}
sort = request.args.get("sort", "created_at")
if sort not in allowed_sort:
sort = "created_at"
query = f"SELECT id, username, created_at FROM users ORDER BY {sort} DESC"
처음엔 번거로워 보여도, 실제 운영을 생각하면 훨씬 낫습니다. 허용된 선택지만 열어두는 방식이 결국 유지보수도 편하더라고요.
5. SQL 인젝션 방어 전략을 운영 관점에서 정리해보겠습니다
5-1. 코드 레벨 방어
- Prepared Statement를 기본값처럼 사용합니다.
- ORM(Object-Relational Mapping)을 쓰더라도 Raw Query(직접 쿼리) 사용 지점을 점검합니다.
- 에러 메시지에 SQL 구문이나 테이블명을 노출하지 않습니다.
- 입력 길이 제한과 형식 검증을 함께 둡니다.
5-2. DB 레벨 방어
- 애플리케이션 계정에
DROP,ALTER같은 불필요 권한을 주지 않습니다. - 읽기 전용 서비스는 Read-Only 계정을 분리합니다.
- 운영 계정과 마이그레이션(Migration) 계정을 분리합니다.
5-3. 인프라 레벨 방어
- WAF(Web Application Firewall)는 보조 수단으로 둡니다.
- 리버스 프록시(Reverse Proxy) 또는 API Gateway에서 비정상 패턴을 로깅합니다.
- DB 접근 경로를 내부 네트워크로 제한합니다.
- 감사 로그(Audit Log)와 애플리케이션 로그를 함께 봅니다.
문자열 결합 방식과 매개변수 바인딩 방식의 차이를 한눈에 보여주는 비교 이미지입니다.
5-4. 어떤 방어가 어디까지 막아주나요?
| 방어 방법 | 막아주는 범위 | 한계 |
|---|---|---|
| Prepared Statement | 값 기반 입력을 통한 인젝션 차단 | 동적 컬럼/정렬명은 별도 검증 필요 |
| 입력 검증 | 비정상 값 조기 차단 | 검증만으로 완전 방어는 어려움 |
| 최소 권한 DB 계정 | 침해 시 피해 범위 축소 | 취약점 자체가 사라지진 않음 |
| WAF | 알려진 패턴 필터링 | 우회 가능, 오탐 가능 |
| 로그 모니터링 | 사후 탐지와 대응 속도 개선 | 예방책은 아님 |
6. ⚠️ 실제로 많이 겪는 문제와 트러블슈팅
제가 현업과 홈랩에서 자주 봤던 삽질 포인트를 정리해봤습니다. 저도 처음엔 헷갈렸는데, 이 부분만 잡아도 재발이 크게 줄었습니다.
6-1. 이스케이프만 하면 된다고 믿는 경우
문자 이스케이프(Escape)는 보조 수단입니다. DB 설정, 문자셋, 드라이버 차이 때문에 예상과 다르게 동작할 수 있습니다. 예전에 레거시 앱 하나에서 수동 이스케이프 함수만 믿고 있었는데, 특정 입력에서 필터 우회가 가능해서 결국 전부 Prepared Statement로 갈아탔습니다. 그때 좀 삽질했습니다 ㅎㅎ
6-2. ORM 쓰니까 안전하다고 생각하는 경우
ORM을 써도 Raw SQL을 직접 붙이면 똑같이 위험합니다. 특히 관리자 검색 기능이나 통계 쿼리에서 예외적으로 직접 작성한 SQL이 문제를 만들더라고요.
6-3. 에러 페이지가 너무 친절한 경우
운영 중 디버그 모드가 켜져 있으면 테이블명, 컬럼명, 스택 트레이스(Stack Trace)까지 노출됩니다. 공격자 입장에서는 힌트 모음집입니다. 운영 환경에서는 일반화된 오류 메시지만 보여주고, 상세 내용은 내부 로그로만 남겨야 합니다.
6-4. 탐지 룰이 없어서 시도 자체를 모르는 경우
이상하게도 방어 코드는 넣어두고 로그는 안 보는 팀이 꽤 있습니다. 아래처럼 웹 로그에서 단순 패턴이라도 먼저 잡아두면 도움이 됩니다.
grep -Ei "union select|or 1=1|-- |sleep\(|benchmark\(" /var/log/nginx/access.log
물론 이 방식은 완전하지 않습니다. 하지만 초기 탐지나 교육용 시연에는 꽤 직관적입니다. 이후엔 SIEM(Security Information and Event Management)이나 중앙 로그 수집으로 확장하면 좋고요.
7. 검증: 수정 후 어떻게 확인할까요?
수정했다고 바로 끝내면 안 됩니다. SQL 인젝션 방어는 재현 테스트와 회귀 테스트(Regression Test)까지 묶어야 진짜 닫힌 겁니다.
- 기존 정상 로그인/조회 기능이 그대로 동작하는지 확인합니다.
- 이전에 먹히던 페이로드가 더 이상 동작하지 않는지 확인합니다.
- 에러 메시지가 일반화되어 있는지 확인합니다.
- DB 계정 권한이 최소화되었는지 확인합니다.
- 로그에 공격 시도가 남는지 확인합니다.
sqlmap -u "http://lab.local/login" \
--data="id=test&pw=test1234" \
--batch \
--risk=1 \
--level=1
중요한 건 자동화 도구 결과를 맹신하지 않는 겁니다. 제가 직접 해보니, 방어가 잘 되어 있어도 앱 특성 때문에 의심 결과가 나오는 경우가 있었고요. 반대로 일부 동적 쿼리는 사람이 맥락을 봐야 진짜 위험성을 알 수 있었습니다.
취약점 재현 실패, 로그 탐지 성공, 권한 축소 적용 여부를 시각적으로 확인하는 결과 이미지입니다.
7-1. 간단 체크리스트
- ✅ 모든 사용자 입력이 바인딩 처리되는가
- ✅ 동적 정렬/필터 값은 허용 목록으로 제한되는가
- ✅ 운영 에러 페이지에 SQL 정보가 노출되지 않는가
- ✅ 앱 DB 계정이 과도한 권한을 갖고 있지 않은가
- ✅ 침투 테스트 후 재검증 기록이 남아 있는가
8. 정리와 다음 단계
이번 모의 해킹 사례에서 핵심은 단순합니다. SQL 인젝션은 화려한 공격이 아니라, 입력을 쿼리 구조와 섞어버린 개발 습관에서 시작됩니다. 그리고 SQL 인젝션 방어는 Prepared Statement 하나로 출발하지만, 최소 권한, 에러 처리, 로깅, 재검증까지 이어져야 제대로 닫힙니다.
혹시 지금 운영 중인 내부 도구가 있다면, 로그인과 검색 기능부터 먼저 보세요. 거기가 가장 자주 터집니다. 저도 처음엔 "이 정도는 괜찮겠지" 했다가 나중에 테스트 로그 보고 식은땀 흘린 적이 있거든요. 드디어 됐다! 싶은 순간은 보통 재현이 안 될 때가 아니라, 왜 안 되는지 근거까지 설명할 수 있을 때 오더라고요.
다음 글에서는 XSS(Cross-Site Scripting)와 CSRF(Cross-Site Request Forgery)를 묶어서 다룰 예정입니다. 이전 글의 로그 분석 편도 같이 보시면 흐름이 더 잘 잡히실 겁니다.
Prepared Statement, 입력 검증, 최소 권한, 모니터링을 한 장으로 요약한 마무리 이미지입니다.
FAQ
Q1. WAF만 있으면 SQL 인젝션 방어가 충분한가요?
아닙니다. WAF는 우회 가능성이 있고, 애플리케이션 코드 수준 문제를 근본적으로 해결하지 못합니다. 보조 장비로 봐야 합니다.
Q2. 내부망 서비스도 꼭 점검해야 하나요?
네. 내부망 서비스는 방심하기 쉬워서 오히려 취약한 경우가 많습니다. 계정 탈취나 VPN 접속 이후 lateral movement의 출발점이 되기도 합니다.
Q3. 침투 테스트는 어디부터 시작하면 좋을까요?
로그인, 검색, 정렬, 필터, 다운로드 파라미터처럼 사용자 입력이 DB 질의와 연결되는 화면부터 시작하시면 됩니다. 이 순서가 실패 확률이 낮습니다.
'IT > 보안' 카테고리의 다른 글
| [보안] Metasploit 오류 해결: 침투 테스트 중 흔히 겪는 문제와 실전 해결법 (0) | 2026.07.16 |
|---|---|
| [보안] OpenVAS 활용 취약점 스캔 효율성 높이는 10가지 체크리스트 (1) | 2026.07.15 |
| [보안] IDS IPS 비교: Snort vs Suricata 선택 가이드 (0) | 2026.07.15 |
| [보안] 웹 취약점 진단: Burp Suite vs OWASP ZAP 심층 비교 (1) | 2026.07.11 |
| [보안] 인증서 관리 자동화로 시간과 비용 절약하기 - Let's Encrypt 완벽 가이드 (0) | 2026.07.10 |
| [보안] YubiKey 도입 체크리스트로 끝내는 하드웨어 2FA 구축 전략 (0) | 2026.07.08 |