검증 2: IDOR, 무엇이 잡나

아프로·2026년 10월 30일

검증 2 IDOR 무엇이 잡나 표지

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

결과 한 장

규칙 여섯 개와 WAF를 IDOR 열거 네 종류에 걸어 몇 번째 요청에서 잡았는지와 오탐 사용자 수를 나타낸 표

조건: 소유권 검사가 있는 앱, 섀도 모드(대응 없이 경보만), 신원(계정) 기준, 2026-10-30 실행. 칸의 숫자는 그 공격자의 몇 번째 요청에서 첫 경보가 났는지입니다.

규칙A1 빠른A2 느린A3 IP 바꿈A4 계정 바꿈오탐 사용자(150명 중)
WAF만(CRS PL1)못 막음못 막음못 막음못 막음0명
JWT.sub != Path.ID1번째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명

WAF만으로는: 660건 중 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·404 급증: 빠른 것만 잡는다

원문은 "403/401 에러율 급증"이라고 했습니다. 실험실 앱은 남의 주문에 404를 돌려줍니다(존재 여부를 흘리지 않으려고, 2편). 403만 세는 규칙이었다면 아무것도 잡지 못했을 겁니다. 그래서 403과 404를 함께 셌습니다.

결과는 예상대로입니다. 빠른 열거(A1)와 IP를 바꾸는 열거(A3)는 10번째 요청에서 잡았습니다. 90초 간격의 느린 열거(A2)는 60초 윈도우에 두 건 이상 들어오지 않으니 영원히 못 잡습니다. 계정을 바꾸는 열거(A4)는 계정마다 4건이라 10에 닿지 않습니다. 오탐은 없었습니다. 정상 사용자의 404는 1분에 10개씩 나오지 않으니까요.

거부된 서로 다른 객체 수: 같은 오탐 0, 더 많이 잡는다

인가 검사가 낸 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번째 요청에서 잡혔고 느린 열거와 계정 바꾸기는 놓쳤습니다.

미끼 주문: 넷 다, 오탐 0

미끼는 시간도, 기준값도, 세는 기준도 없습니다. 그래서 IP를 바꾸든 계정을 바꾸든 느리게 오든 상관없이 네 종류를 모두 잡았습니다. 오탐은 0명입니다. 대신 언제 잡을지는 운입니다. A3는 45번째 요청, A2는 28번째(모의 시각 40분)에야 미끼를 밟았습니다(5편).

소유권 검사가 없다면

소유권 검사가 없는 앱에서 탐지만 했을 때와 단계적 대응을 했을 때 새어 나간 남의 주문 수

마지막으로 앱에서 소유권 검사를 빼고 같은 트래픽을 흘렸습니다. 취약한 앱을 흉내 낸 것입니다.

  • 인가 거부 이벤트가 0건이 됩니다. 거절을 안 하니까요. 그래서 "거부된 서로 다른 객체 수"와 "403·404 급증"은 아무것도 잡지 못했습니다. 남은 신호는 미끼, 연 서로 다른 주문 수, 요청 수뿐이었습니다.
  • 대응 없이(섀도) 남의 주문 657건이 나갔습니다(A1 299, A2 80, A3 200, A4 78).
  • 단계적 대응을 켜도 194건이 나갔습니다(A1 13, A2 62, A3 45, A4 74). 70% 줄었지만 0은 아닙니다. 느린 열거와 계정을 바꾸는 열거는 거의 그대로 새었습니다.
  • 소유권 검사를 넣은 앱에서는 0건입니다.

정리

  • WAF(CRS)는 IDOR 요청을 하나도 막지 못했습니다. 막을 근거가 요청에 없기 때문입니다.
  • JWT.sub != Path.ID는 객체 ID가 따로 있는 경로에서 정상 사용자 97%(146/150)에게 경보를 울렸습니다. 쓰면 안 됩니다.
  • 거부된 서로 다른 객체 수와 미끼 주문은 오탐 0으로 IDOR 열거를 잡았습니다. 둘을 함께 쓰면 네 종류를 모두 덮습니다.
  • 탐지는 예방을 대신하지 않습니다. 소유권 검사가 없는 앱은 탐지·대응을 켜도 194건이 새었고, 검사가 있는 앱은 0건이었습니다.
  • 이 숫자는 이 실험실의 합성 트래픽에서 나온 것입니다.

출처와 확인 날짜

  • 탐지 결과: 실험실 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.json
  • WAF 결과: lab/results/waf/pl1.json(ModSecurity v3 + CRS 4.30.0, owasp/modsecurity-crs:nginx)
  • OWASP API1:2023 BOLA: https://api-security.owasp.org/editions/2023/en/0xa1-broken-object-level-authorization (2026-10-30 확인)

이전 글 검증 1: 실험실과 지표
다음 글 검증 3: 열거·퍼징·느린 공격
시리즈 전체 목록 해킹 시도를 몇 초 안에 잡는 서버 전체 목록

profile
고인물 개발자(OG)

0개의 댓글