
원문은 IDOR 탐지 규칙으로 두 가지를 제안했습니다. JWT.sub != Path.ID, 그리고 "403/401 에러율 급증". 이 시리즈는 대신 "거부된 서로 다른 객체 수"와 "미끼 객체"를 권했습니다. 말로 다투지 않고, 같은 트래픽에 모두 걸어 봤습니다. 실험실과 트래픽은 지난 글에 있습니다.

조건: 소유권 검사가 있는 앱, 섀도 모드(대응 없이 경보만), 신원(계정) 기준, 2026-10-30 실행. 칸의 숫자는 그 공격자의 몇 번째 요청에서 첫 경보가 났는지입니다.
| 규칙 | A1 빠른 | A2 느린 | A3 IP 바꿈 | A4 계정 바꿈 | 오탐 사용자(150명 중) |
|---|---|---|---|---|---|
| WAF만(CRS PL1) | 못 막음 | 못 막음 | 못 막음 | 못 막음 | 0명 |
JWT.sub != Path.ID | 1번째 | 1번째 | 1번째 | 1번째 | 146명 |
| 403·404 급증(60초 10개) | 10번째 | 놓침 | 10번째 | 놓침 | 0명 |
| 거부된 서로 다른 객체(600초 5개) | 5번째 | 5번째 | 5번째 | 놓침 | 0명 |
| 연 서로 다른 주문(600초 30개) | 30번째 | 놓침 | 30번째 | 놓침 | 5명 |
| 미끼 주문(1회) | 13번째 | 28번째 | 45번째 | 7번째 | 0명 |
실험실 앱 앞에 ModSecurity v3 + OWASP CRS 4.30.0을 두고 같은 트래픽을 흘렸습니다. 패러노이아 레벨 1에서 IDOR 요청 660건 중 막힌 것은 0건이었습니다. 같은 WAF가 교과서 공격 표본(SQL 주입, XSS, 경로 조작, 명령 주입, 스캐너 이름) 10건은 10건 모두 막았습니다. WAF가 고장 난 게 아닙니다. GET /api/orders/100458에는 막을 근거가 없을 뿐입니다(7편). 레벨별 결과는 15편에 있습니다.
JWT.sub != Path.ID: 전부 잡고, 거의 전부 오탐이 규칙은 경로의 숫자 ID와 토큰 주체의 숫자를 비교합니다. 실험실의 주문 번호는 100001부터 시작하고, 사용자 ID는 u067 같은 모양이라 숫자 67과 주문 번호는 같을 수가 없습니다. 그래서 주문을 여는 모든 요청에 경보가 울렸습니다. 공격은 첫 요청에 잡혔고, 정상 사용자 150명 중 146명이 경보를 받았습니다(경보 311건). 경보를 받지 않은 4명은 주문 상세를 한 번도 열지 않은 폴링 앱이었습니다.
이 규칙이 맞는 곳은 /users/{id}/profile처럼 경로 ID가 곧 사용자 ID인 경로뿐입니다. 실험실의 프로필 경로에서는 오탐이 없었습니다. 하지만 그런 경로라면 탐지보다 id == sub 인가 검사를 넣는 편이 낫습니다. 주문, 문서, 파일처럼 객체 ID가 따로 있는 경로에서는 소유권을 DB에 물어봐야 합니다. 그 답을 아는 것이 인가 검사이고, 그 결과를 세는 것이 다음 규칙들입니다.
원문은 "403/401 에러율 급증"이라고 했습니다. 실험실 앱은 남의 주문에 404를 돌려줍니다(존재 여부를 흘리지 않으려고, 2편). 403만 세는 규칙이었다면 아무것도 잡지 못했을 겁니다. 그래서 403과 404를 함께 셌습니다.
결과는 예상대로입니다. 빠른 열거(A1)와 IP를 바꾸는 열거(A3)는 10번째 요청에서 잡았습니다. 90초 간격의 느린 열거(A2)는 60초 윈도우에 두 건 이상 들어오지 않으니 영원히 못 잡습니다. 계정을 바꾸는 열거(A4)는 계정마다 4건이라 10에 닿지 않습니다. 오탐은 없었습니다. 정상 사용자의 404는 1분에 10개씩 나오지 않으니까요.
인가 검사가 낸 authz_fail과 object_not_found 이벤트를 신원별로, 서로 다른 객체 수로 셉니다(4편). 윈도우가 10분이라 느린 열거도 5번째 요청(모의 시각 6분)에 잡혔습니다. 오탐은 0명입니다. 가족이 보낸 링크로 남의 주문 1~2건을 연 정상 사용자는 5개에 닿지 않았습니다.
놓친 것은 계정 바꾸기(A4)입니다. 계정마다 4건이니 기준 5에 못 미칩니다. 기준을 3으로 낮추면 A4도 잡히고 오탐은 여전히 0명이었습니다. 2로 낮추면 14명이 오탐으로 걸렸습니다(14편). 또는 같은 규칙을 IP 기준으로도 세면 A4를 5번째 요청에서 잡습니다. 그 대신 IP 기준은 A3를 놓칩니다.
응답 상태와 상관없이 "서로 다른 주문을 몇 개 열었나"를 셉니다. 인가 거부 이벤트가 없어도 쓸 수 있다는 장점이 있습니다. 대신 주문이 60건인 정상 사용자 5명 모두가 걸렸습니다. 자기 주문을 차례로 연 것뿐인데 말입니다. 기준을 100으로 올리면 그들은 빠지지만, 그때는 A1과 A3만 100번째 요청에서 잡혔고 느린 열거와 계정 바꾸기는 놓쳤습니다.
미끼는 시간도, 기준값도, 세는 기준도 없습니다. 그래서 IP를 바꾸든 계정을 바꾸든 느리게 오든 상관없이 네 종류를 모두 잡았습니다. 오탐은 0명입니다. 대신 언제 잡을지는 운입니다. A3는 45번째 요청, A2는 28번째(모의 시각 40분)에야 미끼를 밟았습니다(5편).

마지막으로 앱에서 소유권 검사를 빼고 같은 트래픽을 흘렸습니다. 취약한 앱을 흉내 낸 것입니다.
JWT.sub != Path.ID는 객체 ID가 따로 있는 경로에서 정상 사용자 97%(146/150)에게 경보를 울렸습니다. 쓰면 안 됩니다.lab/results/run-shadow-redis/detectors_default.json(2026-10-30 실행)lab/results/run-vuln-shadow/leak.json, run-vuln-shadow/detectors_default.json, run-vuln-enforce/leak.jsonlab/results/waf/pl1.json(ModSecurity v3 + CRS 4.30.0, owasp/modsecurity-crs:nginx)이전 글 검증 1: 실험실과 지표
다음 글 검증 3: 열거·퍼징·느린 공격
시리즈 전체 목록 해킹 시도를 몇 초 안에 잡는 서버 전체 목록