클라이언트의 요청을 어떻게 거부해야 할까?
지난 해 처음 해보는 팀 프로젝트에서 백엔드 파트로 참여하면서, 여러 고민을 했던 경험이 있다.
그 중 가장 기억에 남는 것은 클라이언트에 반환할 상태 코드 관련 고민이었다.
우리 프로젝트에서는 권한이 Member와 Admin으로 나뉘어 있었다.
RentalStatus는 Member/Admin 둘 다 접근 가능한 필드였으나, 각자 변경할 수 있는 Status가 달랐다.
그래서 Service Layer에 Validation 로직을 길게 짜지 말고, Validation을 dto에서 처리해보면 어떨까 하고 씨름해보았던 기억이 있다.
그런데 본인 권한에 맞지 않는 Status로 변경하면 어떤 상태코드를 반환해야 할지 의문을 가지게 되었다.
적절한 상태코드를 생각하지 못한 관계로 급한대로 400 (Bad Request)로 처리했던 것으로 기억하는데, 이렇게 권한과 연관된 상황에서 과연 어떤 상태코드가 적절할지 고민해보도록 하자.
HTTP 상태 코드
다음 인용들은 https://httpwg.org/specs/rfc9110.html#status.codes 에서 참조했다.
RFC9110
HTTP Semantics
httpwg.org
401 Unauthorized
The 401 (Unauthorized) status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource.
'401 Unauthorized'는 대상 리소스에 대한 인증이 부족하여 요청이 적용되지 않았음을 나타낸다.
클라이언트에게 '이 리소스에 접근하려면 로그인이 필요합니다.' 라는 의미를 전달하는 것과 같다.
403 Forbidden
The 403 (Forbidden) status code indicates that the server understood the request but refuses to fulfill it.
'403 Forbidden'은 요청을 이해했으나 이행하지 않았음, 즉 거부되었음을 나타낸다.
클라이언트에게 '이 리소스에 접근하기 위한 권한이 필요합니다.' 라는 의미를 전달하는 것과 같다.
401 vs 403
If the request included authentication credentials, then the 401 response indicates that authorization has been refused for those credentials.
401은 자격 증명을 가지고 있는 경우에는 그 자격 증명에 대한 승인이 거부되었음을 나타낸다.
If authentication credentials were provided in the request, the server considers them insufficient to grant access.
403은 자격 증명을 가지고 있는 경우에는 그 자격 증명이 리소스에 접근하기 불충분함을 나타낸다.
401과 403이 자격 증명에 대해 비슷한 말을 하는데, 다음과 같이 정리할 수 있겠다.
- 401 Unauthorized: 인증 정보가 없거나, 유효하지 않은 인증 정보를 담음
- 즉, Access Token이 없거나, 유효하지 않은 Access Token을 담고 있음
- 403 Forbidden: 인증 정보는 유효하지만, 권한이 부족함
- 즉, Access Token은 유효하지만, 권한이 부족함
404 Not Found
403 Forbidden 단락에서는 다음과 같은 설명을 볼 수 있다.
An origin server that wishes to "hide" the current existence of a forbidden target resource MAY instead respond with a status code of 404 (Not Found).
만약, 리소스의 존재를 숨기고자 한다면, 404 Not Found로 응답하는 것이 가능하다.
즉, 서버는 권한이 부족한 사용자에게 403이 아니라 404를 통해 응답하는 것이 가능하다는 말이다.
The 404 (Not Found) status code indicates that the origin server did not find a current representation for the target resource or is not willing to disclose that one exists.
'404 Not Found'는 서버가 리소스를 찾지 못했거나 리소스를 공개할 의사가 없음을 나타낸다.
리소스를 찾지 못했을 때 404를 반환하는 것은 당연하고, 여기서 중요한 것은 '리소스를 공개할 의사가 없음을 나타낸다'이다.
클라이언트가 '404 Not Found'로 응답받아도, 클라이언트 입장에서는 직접 서버를 뜯어보지 않고서야 이 리소스가 존재하지 않는건지, 아니면 리소스를 공개하지 않는 것인지 알 수 있는 방법이 없다.
보안 측면
만약에 관리자만 접근 가능한 엔드포인트에 공격자가 요청을 보내본다고 하자.
이러한 엔드포인트는 외부에 노출되지 않기 때문에 공격자는 예상 가능한 다양한 엔드포인트 (/admin, /member/list, /config) 등에 요청을 보내고 응답 코드를 분석해 볼 것이다.
서버에서 이러한 시도에 대해 '401 Unauthorized' 또는 '403 Forbidden'을 반환한다고 하자.
401과 403의 대전제는 일단 '리소스의 존재를 인정한다'는 것이다.
공격자 입장에서는 이런 상태코드를 통해 엔드포인트의 존재를 파악하고, 인증을 우회할 수 있는 취약점을 찾아 공격하기 쉬워질 것이다.
그런데 '404 Not Found'를 반환하는 경우에는 어떨까? 공격자는 해당 엔드포인트가 존재하지 않는다고 판단하기 때문에 이러한 시도를 사전에 차단할 수 있다.
따라서 이러한 리소스 요청을 거부할 때, '404 Not Found'가 다른 응답코드들보다 보안 측면에서 더 안전하다고 볼 수 있다.
그래서 어떻게 할까?
- 401 Unauthorized: 로그인 하지 않은 사용자에 반환
- 403 Forbidden: 권한 없는 사용자에 반환 (공개된 필드에 대한 수정 요청 등)
- 404 Not Found: 숨기고 싶은 리소스에 접근할 시 반환 (관리자 전용 필드)
상황에 따라 유동적으로 바뀔 수 있지만, 보안을 고려했을 때 다음의 방안이 기본적으로 제일 효과적이라고 생각한다.