자신의 REST API에서 1위 API 취약점(BOLA) 찾아내기
요약
OWASP API Security Top 10 중 1위인 BOLA(객체 수준 권한 분해) 취약점을 이해하고 직접 실습하는 가이드입니다. 취약한 API를 구축하고, 공격을 시도하며, 이를 pytest를 통해 회귀 테스트로 방어하는 전 과정을 다룹니다.
핵심 포인트
- BOLA(IDOR)는 인증된 사용자가 타인의 데이터에 접근할 수 있는 심각한 보안 결함입니다.
- 시그니처 기반 스캐너는 권한 로직 누락을 탐지하지 못할 수 있습니다.
- 취약한 API 구축부터 공격 시나리오, pytest를 이용한 수정 및 테스트 과정을 실습합니다.
- Python, Flask, Requests, Pytest를 활용한 실무적인 보안 개발 접근법을 제시합니다.
당신의 스캐너는 초록색 체크 표시를 보여주었습니다. 하지만 당신의 API는 여전히 모든 사용자의 데이터를 유출하고 있습니다.
이것이 실제 환경에서 가장 흔하게 발생하는 API 결함에 대한 불편한 진실입니다. IDOR라고도 불리는 객체 수준 권한 분해 (Broken Object-Level Authorization, BOLA)는 OWASP API Security Top 10에서 1위를 차지하고 있습니다. 이것은 생소한 버그가 아닙니다. 로그인한 사용자가 정상적인 엔드포인트(endpoint)로 완전히 유효해 보이는 요청을 보내지만, 우연히 다른 사람의 기록을 반환하게 되는 상황입니다. 시그니처 기반 스캐너(Signature-based scanners)는 요청 자체에 형식이 잘못된 부분이 없기 때문에 이를 통과시켜 버립니다. 권한 부여 로직 (authorization logic)이 단순히 누락되었을 뿐입니다.
특정 유형의 버그를 이해하는 가장 좋은 방법은 직접 만들고, 망가뜨린 다음, 수정하는 것입니다. 그래서 우리는 그렇게 할 것입니다. 이 과정을 마치면 여러분은 다음을 얻게 됩니다:
- 로컬에서 실행할 수 있는 의도적으로 취약하게 설계된 작은 REST API
- 공격자가 사용하는 방식대로 악용되는 네 가지 실제 API 약점에 대한 단계별 안내
- 이러한 공격(exploits)을 CI 파이프라인을 위한 회귀 테스트(regression tests)로 변환하는
pytest제품군 - 취약한 코드와 나란히 비교한 수정 방법
모든 것은 여러분의 기기에서 여러분의 앱을 대상으로 실행됩니다. 제3자 타겟이나 실제 운영 시스템은 사용하지 않습니다.
사전 요구 사항 (Prerequisites)
Python 3.9 이상과 두 개의 패키지가 필요합니다:
pip install flask requests pytest
이것이 전체 툴체인(toolchain)입니다. 우리는 공격자 역할을 수행하기 위해 requests를 사용하고, 수정 사항을 고정하기 위해 pytest를 사용할 것입니다.
1단계: 취약한 API 구축하기
이 내용을 app.py로 저장하세요. 이것은 인메모리(in-memory) "데이터베이스"를 가진 작은 사용자 및 주문 서비스입니다. 주석을 읽어보세요. 각 주석은 실제 팀이 마감 압박 속에서 실수로 내리는 결정을 나타냅니다.
# app.py
from flask import Flask, request, jsonify
import secrets
...
실행하기:
python app.py
하나의 터미널에서 실행 상태를 유지하세요. 다른 터미널에서 공격을 시도할 것입니다.
2단계: 정당한 토큰(token) 확보하기
여기서 공격자는 익명의 외부인이 아닙니다. 대부분의 BOLA 악용은 자신의 것이 아닌 ID를 건드리는 실제 로그인된 사용자로부터 발생합니다. 따라서 우리는 지극히 평범한 사용자인 Alice로 로그인하겠습니다.
curl -s -X POST http://localhost:5000/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "alice123"}'
{ "token": "9f2c...your-token..." }
이후의 모든 과정은 Alice의 토큰을 사용합니다. 그녀는 인증(Authenticated)되었습니다. 그녀는 자신의 데이터를 볼 수 있는 권한(Authorized)을 가지고 있습니다. 그녀가 그 외에 무엇에 접근할 수 있는지 살펴보겠습니다.
3단계: BOLA (1위 취약점) 공격
Alice 자신의 레코드는 사용자 ID 1입니다. 하지만 엔드포인트(Endpoint)는 어떤 ID든 받아들입니다. Bob의 ID인 2를 요청해 보겠습니다:
# attack.py
import requests
...
{
"id": 2, "username": "bob", "email": "bob@example.com",
"role": "user", "password_hash": "pbkdf2:sha256:...bob",
...
찾았습니다. Alice가 방금 Bob의 계정을 읽었습니다. 이 요청에는 잘못된 부분이 전혀 없습니다. 유효한 토큰, 유효한 경로(Route), 유효한 ID를 사용했습니다. 서버는 그녀를 인증(Authenticate)했지만, 가장 중요한 질문 하나를 던지지 않았습니다: "이 객체가 당신의 것입니까?"
2를 3으로 바꾸면 그녀는 관리자(Admin)의 정보를 읽습니다. 1부터 N까지 반복하면 그녀는 전체 사용자 테이블을 긁어모을(Scrape) 수 있습니다.
이것이 바로 BOLA가 패턴 매칭(Pattern matching)을 통과하는 이유입니다. 서명(Signature)할 페이로드(Payload)가 없습니다. 버그는 인증(Authentication)과 인가(Authorization) 사이의 간극에 존재합니다.
4단계: 깨진 인증 (Broken Authentication) 공격
주문(Orders) 엔드포인트를 살펴보겠습니다. 누군가 급하게 구현하느라 토큰 확인 과정을 완전히 누락했습니다. 로그인이 필요하지 않습니다:
import requests
# Authorization 헤더가 전혀 없음
print(requests.get("http://localhost:5000/api/users/2/orders").json())
[ { "id": 102, "item": "Headphones", "total": 199 } ]
익명의 요청이 Bob의 주문 내역을 가져왔습니다. 이것이 OWASP API 2위인 깨진 인증 (Broken Authentication)입니다. 데코레이터(Decorator) 하나가 누락되었을 뿐인데, 데이터셋 하나가 노출되었습니다.
5단계: 과도한 데이터 노출 (Excessive Data Exposure) 악용
BOLA 응답으로 돌아가 자세히 읽어보세요. Alice가 자신의 레코드를 요청했을 때조차, API는 password_hash와 ssn을 반환합니다. 핸들러(Handler)가 데이터베이스의 로우(Row)를 그대로 직렬화(Serialize)하여 전송한 것입니다. 이것이 바로 과도한 데이터 노출 (Excessive Data Exposure)입니다. 클라이언트가 UI에서 필터링을 하더라도, API는 모든 것을 보냈으며 응답 본문(Response body)을 읽는 누구라도 그 모든 내용을 볼 수 있습니다.
6단계: 대량 할당 (Mass Assignment) 악용
PATCH 엔드포인트(Endpoint)는 사용자의 JSON을 받아 레코드에 그대로 병합(Merge)합니다. 이 엔드포인트는 사용자가 이메일을 업데이트할 것이라고 예상합니다. 하지만 사용자가 자신의 역할(Role)을 업데이트하는 것을 막는 장치는 아무것도 없습니다.
import requests
BASE = "http://localhost:5000"
...
admin
이제 Alice는 관리자(Administrator)가 되었습니다. 이것이 대량 할당 (Mass Assignment)입니다. 개발자가 노출할 의도가 전혀 없었던 필드를 클라이언트가 제어하게 된 것입니다. 단 한 번의 요청으로 권한 상승 (Privilege escalation)이 발생했습니다.
7단계: 취약점 공격을 테스트 스위트(Test suite)로 전환하기
수동으로 찔러보는 것은 한 번쯤 재미있을 수 있습니다. 하지만 진짜 보상은 이를 영구적인 것으로 만드는 것입니다. 우리는 우리가 원하는 보안 동작을 pytest 단언문(Assertion)으로 인코딩합니다. 취약한 앱을 대상으로 실행하면 테스트는 실패하며, 이는 코드가 잘못되었을 때 좋은 보안 테스트가 수행해야 할 정확한 동작입니다.
test_api_security.py라는 이름으로 저장하세요:
# test_api_security.py
import requests
...
취약한 앱이 실행 중인 상태에서 실행하세요:
pytest test_api_security.py -q
FFFF
4 failed in 0.12s
네 개의 빨간색(실패)이 떴습니다. 각각은 실제 취약점이며, 단언문 메시지에 평이한 언어로 설명되어 있습니다. 이것이 바로 여러분이 커밋(Commit)해야 할 파일입니다. 오늘은 실패하고, 누군가 버그를 다시 도입하는 날에도 다시 실패할 것입니다.
팁: CI(지속적 통합) 환경에서는
pytest를 실행하기 전 백그라운드 단계에서 앱을 시작하거나, 테스트 스위트가 완전히 독립적으로 작동할 수 있도록requests대신 Flask의app.test_client()를 사용하세요.
8단계: 코드 수정하기
이제 초록색(성공)을 얻을 차례입니다. 여기 수정된 네 개의 핸들러가 있습니다.
BOLA와 데이터 노출 문제입니다. 소유권 확인(Ownership check)을 추가하고, 로우(Row) 전체 대신 명시적인 허용 목록(Allow-list) 필드만 직렬화하세요:
PUBLIC_FIELDS = ("id", "username", "email")
def public_view(user):
...
인증 오류 (Broken authentication). 토큰을 요구하고, 여기서도 소유권을 확인하세요:
@app.get("/api/users/<int:user_id>/orders")
def get_orders(user_id):
caller = current_user()
...
대량 할당 (Mass assignment). 가공되지 않은 입력값(raw input)을 병합하지 마세요. 사용자가 변경할 수 있도록 허용된 필드만 허용하세요:
EDITABLE_FIELDS = ("email",)
@app.patch("/api/users/<int:user_id>")
...
앱을 재시작하고 테스트 스위트(suite)를 다시 실행하세요:
pytest test_api_security.py -q
....
4 passed in 0.10s
네 개의 통과(green) 결과가 나왔습니다. 이제 귀하의 API는 객체 수준에서 권한 부여 (authorization)를 강제하고, 모든 곳에서 인증 (authentication)을 요구하며, 응답에서 비밀 정보를 제외하고, 클라이언트가 제공하는 권한 변경을 거부합니다.
수동 테스트가 남기는 간극
여기에 솔직한 함정이 있습니다. 이것이 성숙한 팀에서도 API 침해 사고가 계속 발생하는 이유입니다. 위의 테스트 스위트는 귀하가 테스트할 것이라고 생각한 결함들을 테스트합니다. 하지만 귀하가 상상하지 못한 조합에 대해서는 아무것도 말해주지 않습니다.
이 튜토리얼의 네 가지 버그를 체인(chain)으로 연결하면, 단순히 네 개의 중간 수준(medium) 발견 사항이 되는 것이 아닙니다. 귀하는 하나의 치명적인 경로 (critical path)를 갖게 됩니다. 즉, 인증되지 않은 요청이 주문을 열거하고, BOLA 읽기 공격이 사용자 테이블을 수집하며, 대량 할당 (mass-assignment) PATCH 공격이 관리자 권한 상승으로 이어지는 경로입니다. 단일 테스트 중 어느 것도 이 시퀀스에 대해 단언할 수 없습니다. 왜냐냐하면 이를 작성하려면 엔드포인트를 개별적으로 확인하는 사람이 아니라, 귀하의 앱을 타고 이동하는 공격자처럼 생각해야 하기 때문입니다.
그것이 바로 ZeroThreat.ai가 만들어진 목적입니다. 이 엔진은 애플리케이션 인지형 공격 체인 발견 (application-aware attack chain discovery)을 수행합니다. 실제 사용자가 하는 방식대로 복잡하고 인증이 필요한 다단계 워크플로를 탐색하고, 이러한 약점들을 실제 공격 경로로 연결하며, 도달하는 대상의 비즈니스 영향도에 따라 각 경로의 순위를 매깁니다. 따라서 계정 탈취 (account-takeover) 체인이 노이즈 위로 드러나게 됩니다. 발견된 사항들은 오탐 (false positive) 없이 검증되어 제공되며, 각 항목에는 엔드포인트, 파라미터, 증거 및 수정 방법이 포함되어 전달됩니다. 이것은 귀하의 모의 해킹 전문가 (pentester)나 pytest 스위트를 대체하는 것이 아닙니다. 그들 중 누구도 수동으로는 결코 도달할 수 없었던 영역을 보완하는 것입니다.
핵심 요약 (Takeaways)
- BOLA는 페이로드 (payload)가 아니라 권한 부여 (authorization)의 공백입니다. 인증 (Authenticate)을 수행한 후에는, 항상 해당 객체가 호출자에게 속해 있는지 확인하십시오. 스캐너는 이를 감지할 수 없으므로, 의도적으로 테스트해야 합니다.
- 허용 목록 (allow-listed)에 있는 필드만 반환하고, 원시 행 (raw rows)을 절대 그대로 반환하지 마십시오. API가 경계선입니다. UI에서 클라이언트가 필터링하는 것은 응답 본문 (response body)을 보호하지 못합니다.
- 허용 목록 (allow-listed)에 있는 입력값만 수용하고, 원시 JSON (raw JSON)을 절대 병합 (merge)하지 마십시오. 대량 할당 (Mass assignment)은 단 하나의 잊혀진 필드만으로도 권한 상승 (privilege escalation)으로 이어질 수 있습니다.
- 보안 요구 사항을 테스트 코드로 인코딩하십시오. 실패하는 보안 테스트는 실제 위험에 대한 문서화입니다. 이를 커밋하면 향후 모든 PR (Pull Request)을 보호할 수 있습니다.
- 엔드포인트 (endpoints)뿐만 아니라 체인 (chains)을 테스트하십시오. 위험한 버그는 대개 당신이 확인할 생각을 하지 못했던 조합에서 발생합니다.
두 파일을 복제하여, 망가뜨려 보고, 수정하고, 테스트 스위트 (suite)를 유지하십시오. 그런 다음 애플리케이션의 맥락을 이해하는 도구를 전체 앱에 적용하여 당신이 놓쳤을 경로를 찾아내십시오.
제가 여기서 다루지 않은 당신이 즐겨 찾는 API 실수 (footgun)가 있나요? 댓글로 남겨주세요, 모두 읽어봅니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기