
2부까지 "이 규칙이 낫다", "저 규칙은 오탐이 많다"고 여러 번 말했습니다. 이제 그 말을 숫자로 확인합니다. 탐지 규칙도 코드입니다. 코드는 시험해야 합니다. 이 방식을 탐지를 코드처럼 다루기(detection-as-code)라고 부릅니다. 규칙을 바꿀 때마다 같은 정상 트래픽에서 오탐을, 같은 공격 트래픽에서 탐지를 다시 잽니다.

모든 것이 이 맥 한 대의 127.0.0.1에서 돕니다. 바깥 주소로는 요청을 한 건도 보내지 않습니다. 코드는 실험실 폴더 lab/에 있고, README.md에 재현 명령이 있습니다.
app.py, 포트 18110). FastAPI로 만든 가짜 쇼핑몰입니다. 사용자 200명, 주문 1,365건(그중 27건은 미끼, 5편), 상품 500개. 주문 조회에는 소유권 검사가 있고(환경 변수로 뺄 수 있음), 모르는 파라미터는 400, 틀린 값은 422로 답합니다.detect.py). 3·4편의 보안 이벤트와 슬라이딩 윈도우 규칙 7개, 6편의 단계적 대응. 윈도우는 Redis(포트 18379, 실험용으로 따로 띄움)나 메모리 중에 고릅니다.traffic.py). 정상과 공격 요청에 이름표를 붙여 보냅니다.evaluate.py). 요청 기록을 읽어 지표를 계산하고, 기준값을 바꿔 가며 다시 흘려 봅니다.bench.py), WAF 실험(waf/).시드를 고정해 매번 같은 트래픽이 나옵니다. 모두 5,373건입니다.
정상(3,913건, 150명)
| 종류 | 사람 수 | 모양 |
|---|---|---|
| 둘러보기 | 141명 | 주문 목록, 내 주문 1~4건 열기, 상품 검색(한국어 검색어 포함), 가끔 재시도 |
| 주문 많은 사용자 | 5명 | 주문 60건을 목록에서 하나씩 모두 열기 |
| 모바일 폴링 | 3명 | 2초마다 주문 목록, 15분 |
| 잘못 만든 앱 | 1명 | 0.4초마다 주문 목록, 3분 |
| 섞여 있는 예외 | 일부 | 가족이 보낸 남의 주문 링크(28건), 지운 주문의 옛 링크(18건), 번호 잘못 입력(8건), 옛 앱의 모르는 파라미터(6건) |
141명 가운데 50명은 CGNAT 공유 IP 두 개(100.64.0.1, 100.64.0.2) 뒤에 있습니다.
공격(1,460건, 6종)
| 이름 | 모양 |
|---|---|
| A1 빠른 열거 | 계정 1, IP 1, 0.2초마다 주문 번호 +1, 300건 |
| A2 느린 열거 | 90초마다 +1, 80건(2시간에 걸쳐) |
| A3 IP 바꾸는 열거 | 계정 1, 요청마다 다른 IP, 1초 간격 200건 |
| A4 계정 바꾸는 열거 | IP 1, 계정 20개가 4건씩, 2초 간격 80건 |
| A5 파라미터 퍼징 | 처음 보는 파라미터 이름과 형식이 틀린 값, 0.3초 간격 200건 |
| A6 폭주 수집 | 상품 목록 600건을 60초에 |
공격 예시는 교과서 수준입니다. 주입 문자열이나 특정 WAF를 피하는 기법은 쓰지 않았습니다. 퍼징의 파라미터 이름도 debug, role, limit 같은 평범한 단어입니다.
느린 열거를 실제로 2시간 기다릴 수는 없습니다. 그래서 요청마다 X-Lab-Time 헤더에 모의 시각을 실어 보내고, 탐지 규칙은 이 시각으로 윈도우를 셉니다. 2시간 분량이 이 맥에서 5~8초 만에 끝납니다. 이 시리즈에서 "몇 초 만에 잡았다"는 모두 모의 시각입니다. 요청 순서와 간격은 그대로이니 윈도우 규칙의 동작은 같지만, 실제 시스템의 처리 지연은 들어 있지 않습니다. 실제 시간의 비용은 따로 쟀습니다(15편).
클라이언트 IP도 X-Lab-Client-IP 헤더로 넣었습니다. 앞단 프록시가 넣어 준 IP라고 가정한 것입니다. 실서비스에서는 믿을 수 있는 프록시가 넣은 값만 써야 합니다.
p50, p99는 지연을 줄 세웠을 때 50번째, 99번째 백분위 값입니다. p50은 보통의 요청, p99는 운 나쁜 요청의 지연입니다.
소유권 검사를 뺀 설정에서는 추가로 새어 나간 남의 주문 수를 셉니다(13편).
| 설정 | 탐지 윈도우 | 소유권 검사 | 대응 |
|---|---|---|---|
| shadow-redis | Redis | 있음 | 없음(섀도) |
| shadow-memory | 메모리 | 있음 | 없음 |
| vuln-shadow | Redis | 없음 | 없음 |
| vuln-enforce | Redis | 없음 | 단계적 대응 |
| secure-enforce | Redis | 있음 | 단계적 대응 |
| secure-enforce-tuned | Redis | 있음 | 단계적 대응(distinct_objects 뺌) |
shadow-redis와 shadow-memory의 경보는 완전히 같았습니다. 그리고 기록을 메모리 윈도우로 다시 흘린 재생 결과도 앱 안에서 실제로 난 경보와 같았습니다. 그래서 기준값을 바꾼 표(14편)는 서버를 다시 돌리지 않고 재생으로 만들었습니다.
lab/requirements.lock.txt.cd lab
python3 -m venv .venv && .venv/bin/pip install -r requirements.lock.txt
./run_all.sh # 설정 6가지 + 지연 측정, results/에 원시 결과
./waf/run_waf.sh # (Docker 필요) ModSecurity + CRS 패러노이아 레벨 1~4
lab/(README, results/), 2026-10-30 실행이전 글 클라우드·상용으로 같은 그림 그리기
다음 글 검증 2: IDOR, 무엇이 잡나
시리즈 전체 목록 해킹 시도를 몇 초 안에 잡는 서버 전체 목록