TT Lab
블로그 시작하기
배우기 러닝패스 코스

인가와 입력 처리를 코드로 지키기

로그인은 누구인가, 인가는 이것을 해도 되는가

TT Lab 에서 이어서 보기

한 줄 요약

로그인했다는 사실은 「누구인가」 에 대한 답일 뿐입니다. 「이 주문을 읽어도 되는가」 와 「이 기능을 써도 되는가」 는 요청마다 서버가 다시 답해야 하는 별개의 질문이고, 그 답의 근거는 서버가 저장한 사실(주문의 주인, 사용자의 역할)이어야 합니다.

왜 이 검사가 따로 필요한가

주문 상세의 주소가 /orders/o-100 이라면, 로그인한 사람은 누구든 주소의 번호를 바꿔 볼 수 있습니다. 서버가 토큰이 유효한지만 보고 주문을 꺼내 주면 번호를 바꾸는 것만으로 남의 주문이 열립니다. OWASP 는 이런 결함을 IDOR(Insecure Direct Object Reference)라고 부르고, 2021년판 Top 10 의 첫 항목인 Broken Access Control 에 묶어 둡니다. 번호를 uuid 처럼 추측하기 어렵게 만드는 것은 도움이 되지만 해결은 아닙니다. 번호는 주소창과 공유 링크와 접근 로그에 남고, 한 번 새면 그때부터 열쇠 노릇을 합니다.

관리자 기능도 같은 구조입니다. 환불 단추가 운영 화면에만 보여도, 그 단추가 부르는 API 는 누구나 부를 수 있습니다. 운영 화면이 X-Role: admin 헤더를 붙여 보낸다고 서버가 그 헤더를 믿으면, 헤더를 직접 붙이는 사람은 누구나 관리자가 됩니다. 요청에 실려 온 값은 요청을 보낸 쪽이 정하는 값이라 권한의 근거가 될 수 없습니다. 화면에서 단추를 숨기는 일은 사용성의 문제이고, 권한은 서버의 문제입니다.

어떻게 지키나

세 가지를 지킵니다. 첫째, 판단을 한 함수에 모으고 모든 경로가 그 함수를 지나게 합니다. 상세 경로에만 검사를 넣으면 영수증, 내보내기, 첨부 파일 같은 옆 경로가 빠집니다. 경로가 하나 늘 때마다 검사를 기억해야 하는 구조는 언젠가 반드시 하나를 놓칩니다.

def can_read(user, order):
    # 근거는 서버가 저장한 값뿐이다
    return order["owner"] == user["id"] or user.get("role") == "admin"


def load_order(orders, user, order_id, action):
    order = orders.get(order_id)
    if order is None or not can_read(user, order):
        raise HTTPException(404, "Not Found")    # 없는 것과 볼 수 없는 것을 같은 응답으로
    return order

둘째, 없는 주문과 볼 수 없는 주문에 같은 응답을 줍니다. 남의 주문에 403 을 주면 '그 번호의 주문은 있다' 는 사실이 새어 나갑니다. 다만 응답을 같게 하는 것과 기록을 같게 하는 것은 다른 이야기입니다. 감사 로그에는 '권한 때문에 거절했다' 를 따로 남겨야 운영자가 이상한 시도를 알아챕니다.

셋째, 관리자 기능은 서버가 저장한 역할로만 엽니다. 운영 화면이 헤더를 계속 보내도 괜찮습니다. 서버가 그 헤더를 보지 않으면 됩니다.

이 판단을 시험으로 적으면 이렇게 됩니다. 「bob 이 alice 의 주문을 읽으면 404 이고, 그 응답은 없는 주문 o-999 의 응답과 같다」, 「고객이 X-Role 헤더를 붙여 환불하면 403 이고 주문 상태는 그대로다」. 시험은 경로마다 하나씩 둡니다. 그래야 다음 사람이 새 경로를 더하면서 판단 함수를 빠뜨렸을 때 시험이 먼저 알려 줍니다.

현장에서 만나는 모습

목록 API 는 사용자 기준으로 거르면서 상세와 내보내기 API 는 번호로 바로 꺼내는 코드가 흔합니다. 목록을 만든 사람과 상세를 만든 사람이 다르고, 목록에서 이미 걸렀으니 상세도 안전하다고 여기기 때문입니다. 관리자 API 를 별도 경로에 두고 게이트웨이에서만 막는 구성도 자주 봅니다. 게이트웨이 설정이 한 번 바뀌거나 내부망에서 직접 부르는 길이 생기면 그 보호는 사라지므로, 서비스 안에서도 같은 판단을 한 번 더 합니다.

'FastAPI 로 서비스 만들기' 코스의 fa-ownership 실습은 한 경로 안에서 인증(401), 권한(403), 소유(404)의 순서를 세웠습니다. 이 모듈은 그 판단을 여러 경로와 관리자 기능으로 넓히고, 판단 자체를 요구사항 시험으로 고정합니다. 세션과 토큰을 어떻게 발급하고 폐기하는지는 '세션과 토큰' 코스가 다룹니다.

English corner

다음 실습에서 할 것

주문 서비스에서 상세와 영수증이 남의 주문을 꺼내 주고, 환불이 X-Role 헤더를 믿고, 전체 목록은 로그인만 확인합니다. 요구 A(자기 주문만)와 요구 B(저장된 역할로만)를 시험으로 쓰고, 판단 함수에 모아 고치고, 거절을 감사 로그에 남깁니다.