ロールだけではリソースを守れない
한국어 원문으로 표시합니다.
한 줄 요약
인증은 요청 주체를 확인하고 인가는 그 주체가 이 조직의 이 리소스에 이 동작을 할 수 있는지 판단한다. writer 역할 하나만 검사하면 같은 역할을 가진 사용자가 다른 사람의 주문을 읽고 바꾸는 수평 권한 상승이 생긴다.
왜 역할 검사가 불충분한가
역할 기반 접근 제어는 기능 범위를 좁히지만 리소스 관계를 표현하지 못한다. Alice와 Bob이 모두 writer여도 Bob이 Alice의 주문을 취소할 이유는 없다. 조직 관리자도 자기 조직 안에서만 관리 권한을 가져야 한다. 따라서 주문의 owner_id와 org_id, 신원의 sub, org_id, role, 요청 동작을 함께 비교한다. 다른 조직이면 관리자라도 먼저 거부하고, 같은 조직이면 소유자 또는 허용된 관리자 정책을 적용한다.
인증 부재는 401, 인증됐지만 관계가 맞지 않으면 403이다. 존재하지 않는 리소스와 권한 없는 리소스의 응답을 같게 만들어 열거 공격을 줄일지 여부도 정책으로 정한다. 이번 과제의 읽기 경계는 권한 없는 ID를 403으로 일정하게 처리한다. 테스트는 같은 조직의 다른 소유자, 다른 조직의 관리자, 정상 소유자 세 가지를 반드시 포함한다.
현장에서 놓치기 쉬운 누출
인증 헤더 전체를 로그에 찍거나 토큰 파싱 오류에 원문을 포함하면 관측 시스템이 비밀 저장소가 된다. 모듈 import 시 디버그 print가 실행되는 것도 채점기나 워커 로그에 민감한 환경값을 흘릴 수 있다. 라이브러리 import는 조용해야 하고, 오류 응답은 unauthorized, forbidden처럼 분류만 제공한다. 감사 로그에는 토큰 대신 비민감 주체 ID와 정책 결과를 기록한다.
권한 판정을 어디에 두는가
같은 규칙이라도 코드 어느 층에 두느냐가 새는 자리를 만든다. 세 자리가 있고 각각 막는 것이 다르다.
경로 앞의 미들웨어. 인증과 역할 확인처럼 리소스를 안 봐도 되는 것은 여기가 맞다. 모든 경로에 일괄로 걸 수 있어 빠뜨릴 자리가 없다. 다만 리소스 관계는 여기서 판정할 수 없다. 주문 하나를 읽어야 소유자를 알 수 있기 때문이다.
처리 함수 안. 리소스를 읽은 뒤 소유자와 조직을 비교하는 자리다. 표현력은 가장 좋지만 경로마다 사람이 적어야 하므로 빠뜨리기 쉽다. 새 경로를 추가한 사람이 그 줄을 잊으면 그 경로만 열린다. 그래서 이 방식을 쓸 때는 권한 검사를 거치지 않은 경로가 없는지 시험으로 확인하는 장치가 함께 있어야 한다.
저장소 질의 안. 조회 조건에 소유자와 조직을 넣는 방식이다. 애플리케이션 검사와 저장소 범위가 어긋날 수 없다는 것이 가장 큰 장점이고, 앞의 두 자리를 빠뜨려도 남의 데이터가 나오지 않는다. 대신 "없음" 과 "권한 없음" 이 같아지는 것을 감수해야 하며, 그것이 열거 공격을 줄이는 쪽으로는 유리하지만 사용자에게 왜 안 되는지 설명하기는 어려워진다.
실무에서는 셋을 겹쳐 쓴다. 미들웨어로 인증을 보장하고, 저장소 질의로 범위를 자르고, 처리 함수에서 동작별 정책을 판정한다. 한 겹만 두면 그 겹을 빠뜨린 경로가 곧 구멍이고, 겹쳐 두면 하나를 잊어도 나머지가 잡는다.
그리고 거부한 것을 세어 둔다. 정상적으로 거부되는 요청은 언제나 조금 있지만, 그 수가 갑자기 늘면 누가 남의 것을 훑고 있거나 우리 정책이 방금 잘못 바뀐 것이다. 둘 다 알아야 할 일이고, 세지 않으면 둘 다 모른다.
실무 판단 기준
권한 테스트는 “writer가 성공한다”에서 끝나면 안 된다. 가장 위험한 이웃 관계와 테넌트 경계를 반례로 고정해야 한다. 리소스 소유권은 데이터베이스 조회 조건에도 포함해 애플리케이션 검사와 저장소 범위가 어긋나지 않게 한다. 다음 모듈에서는 이런 거부와 성공을 같은 trace 문맥으로 관찰하되 비밀은 남기지 않는 방법을 다룬다.