방어선 1-①: 인가 거부를 보안 이벤트로 남기기

아프로·2026년 10월 30일

방어선 1-① 인가 거부를 보안 이벤트로 남기기 표지

지난 글에서 주문 조회에 소유권 검사를 넣었습니다. 남의 주문이면 404를 돌려줍니다. 이제 질문이 바뀝니다. "방금 거절한 사실을 누가 알고 있나?" 대부분의 앱에서 답은 "아무도"입니다. 액세스 로그에는 404 한 줄만 남고, 그 404가 "없는 주문"이었는지 "남의 주문"이었는지는 사라집니다. 이번 글은 그 차이를 이벤트로 남기는 법입니다.

"RASP"라는 말의 실체

원문은 이 층을 RASP(Runtime Application Self-Protection)라고 부릅니다. 엄밀히 RASP는 앱 런타임 안에 들어가 위험한 호출(SQL 실행, 파일 열기, 외부 요청, 명령 실행)을 직접 지켜보고 막는 기술을 말합니다. 예를 들어 Datadog의 Exploit Prevention은 추적 라이브러리 안에서 SSRF, 로컬 파일 포함(LFI), SQL 주입, 명령 주입을 막는다고 설명합니다.

그런데 원문이 말하는 "세션 사용자 ID와 요청 객체 ID를 비교한다"는 RASP 제품이 아니어도 할 수 있습니다. 우리 코드의 인가 검사가 결과를 밖으로 알리기만 하면 됩니다. 이 시리즈는 이것을 RASP라고 부르지 않고 "인가 결정 이벤트"라고 부릅니다. 이름이 정확해야 제품 비교도 정확해집니다(11편).

무엇을 남기나

요청이 인가 검사를 지나며 보안 이벤트를 남기고, 미들웨어가 그 이벤트를 모아 윈도우로 보내는 흐름

실험실 앱은 네 가지 이벤트를 남깁니다.

이벤트언제출처
authz_fail객체가 있지만 이 사용자 것이 아닐 때OWASP Logging Vocabulary
input_validation_fail모르는 파라미터 이름, 형식이 틀린 값OWASP Logging Vocabulary
object_not_found요청한 객체가 없을 때이 시리즈에서 붙인 이름
honeytoken_access미끼 객체에 접근했을 때(5편)이 시리즈에서 붙인 이름

OWASP Logging Vocabulary는 보안 이벤트 이름을 표준처럼 맞춰 쓰자는 문서입니다. 이름을 맞춰 두면 나중에 SIEM(보안 로그를 모아 분석하는 시스템)에서 서비스 여러 개의 이벤트를 한 규칙으로 엮을 수 있습니다. 표준에 없는 이름은 우리 쪽 이름이라고 문서에 적어 둡니다. 참고로 이 어휘집에는 없는 파일 요청이 반복될 때의 malicious_excess_404, 직접 객체 참조 악용을 뜻하는 malicious_direct_reference 같은 탐지 결과 이름도 있습니다. 4편의 규칙이 경보를 낼 때 이런 이름을 쓰면 됩니다.

FastAPI로 남기기

핸들러는 이벤트를 기록만 하고, 판단은 미들웨어가 합니다. 실험실 lab/app.py의 핵심만 옮깁니다.

def sec_event(request, type_, **kw):
    # 주체 ID와 객체 ID만. 이름·주소·토큰은 넣지 않는다.
    request.state.events.append({"type": type_, **kw})

@app.get("/api/orders/{order_id}")
async def get_order(order_id: str, request: Request):
    user = current_user(request)
    if not order_id.isdigit():
        sec_event(request, "input_validation_fail", names=["order_id"])
        return JSONResponse({"detail": "invalid id"}, status_code=422)
    order = ORDERS.get(int(order_id))
    if order is None:
        sec_event(request, "object_not_found", object=int(order_id))
        return JSONResponse({"detail": "not found"}, status_code=404)
    if order["owner"] != user:
        sec_event(request, "authz_fail", object=int(order_id))
        return JSONResponse({"detail": "not found"}, status_code=404)   # 응답은 같다
    return order

미들웨어는 요청이 끝난 뒤 한 줄짜리 기록을 만듭니다.

{"t": 1032.4, "sub": "u151", "ip": "203.0.113.10",
 "method": "GET", "route": "/api/orders/{order_id}", "path_id": "100458",
 "status": 404, "events": [{"type": "authz_fail", "object": 100458}]}

몇 가지 설계 선택이 있습니다.

  • 경로는 원래 경로가 아니라 라우트 틀(/api/orders/{order_id})로 남깁니다. 그래야 "같은 엔드포인트"로 묶어 셀 수 있습니다.
  • 응답 상태와 이벤트를 따로 남깁니다. 상태만 보면 404가 "없음"인지 "남의 것"인지 모릅니다.
  • 기록은 모아서 씁니다. 실험실은 500줄마다 파일에 씁니다. 요청마다 파일을 열면 그만큼 느려집니다.
  • 이벤트는 응답을 바꾸지 않습니다. 탐지가 실패해도 서비스는 그대로 돕니다.

OpenTelemetry 이름과 맞추기

이벤트를 나중에 OpenTelemetry(OTel, 추적·지표·로그를 모으는 공개 표준)로 보낼 생각이라면 처음부터 속성 이름을 맞춥니다. OTel HTTP 서버 규약은 안정(Stable) 상태입니다.

실험실 필드OTel 속성상태
methodhttp.request.method필수
routehttp.route있으면 필수
statushttp.response.status_code응답했으면 필수
ipclient.address권장
subenduser.id 또는 user.id개발 중(Development)

사용자 ID 속성은 아직 개발 중 단계입니다. enduser.id는 폐기되지 않았고, enduser.role은 user.roles로 옮겨 가는 중입니다. 이름이 바뀔 수 있으니 내보내는 지점 한곳에서 바꿀 수 있게 해 두세요.

개인정보는 빼고, 추적은 되게

보안 이벤트는 오래 보관되고 여러 사람이 봅니다. 그러니 넣지 않을 것부터 정합니다.

  • 토큰, 비밀번호, 세션 쿠키: 절대 넣지 않습니다.
  • 이름, 주소, 전화번호, 주문 내용: 넣지 않습니다. 객체 ID면 충분합니다.
  • 사용자 ID: 내부 ID를 씁니다. 외부 공유가 필요하면 가명 ID(키 있는 해시)로 바꿉니다.
  • IP: 개인정보로 다루는 곳이 많습니다. 보관 기간을 짧게 잡습니다.

OWASP Logging Cheat Sheet도 비밀번호·세션 ID·접근 토큰 같은 값은 로그에서 빼거나 가리라고 권합니다.

오탐을 줄이는 작은 습관

이벤트는 많을수록 좋은 게 아닙니다. 실험실에서 쓸모 있었던 원칙입니다.

  • 결정이 있는 곳에서만 냅니다. 인가 검사, 검증, 미끼. "의심스러워 보이는 모든 것"을 내면 경보 피로가 옵니다.
  • 사람이 정상적으로 내는 이벤트를 알아 둡니다. 실험실 정상 트래픽에도 거부·없음 이벤트가 있습니다. 가족이 보낸 주문 링크를 연 요청(28건, authz_fail), 지운 주문의 옛 링크(18건, object_not_found), 번호를 잘못 친 요청(8건)입니다.
  • 한 요청에 여러 이벤트가 붙을 수 있게 합니다. 미끼 주문을 남이 열면 honeytoken_access와 authz_fail이 함께 붙습니다.

정리

  • 인가 결정은 WAF가 볼 수 없는, 앱만 가진 가장 정확한 신호입니다. 응답은 같은 404여도 내부 이벤트는 다르게 남기세요.
  • 이벤트 이름은 OWASP Logging Vocabulary에, 속성 이름은 OpenTelemetry 규약에 맞춥니다.
  • 개인정보와 비밀은 넣지 않습니다. 주체 ID와 객체 ID면 충분합니다.
  • 다음 편에서 이 이벤트를 신원별 슬라이딩 윈도우로 셉니다.

출처와 확인 날짜

모든 링크는 2026-10-30에 확인했습니다.


이전 글 방어선 0: 탐지보다 먼저, 코드에서 막기
다음 글 방어선 1-②: 신원별 슬라이딩 윈도우로 열거·퍼징 잡기
시리즈 전체 목록 해킹 시도를 몇 초 안에 잡는 서버 전체 목록

profile
고인물 개발자(OG)

0개의 댓글