![[실전 보안 12] 로그인했다고 다 되는 건 아니다](https://img.thenullpage.com/posts/5719/5719_1_75ce23.webp)
지난 편 끝에서 인증이 너는 누구냐를 묻는다면 권한은 너는 이걸 해도 되느냐를 묻는다고 했다. 이번 편의 주제가 바로 그 권한이다. 로그인이라는 첫 관문을 통과한 사용자라도, 아무 일이나 해도 되는 건 아니다. 남의 글을 함부로 고치거나, 일반 사용자가 운영자 기능에 손대거나, 자기 것이 아닌 자원에 접근하는 걸 막아야 한다. 커뮤니티를 만들며 이 권한 설계를 소홀히 했다가 아찔했던 순간들이 있었다. 그 경험을 바탕으로 권한을 어떻게 다뤄야 하는지 정리해 본다.
1. 인증과 권한은 다른 문제다
초보 시절 나는 인증만 되면 보안이 끝난 줄 알았다. 로그인한 사용자니까 믿어도 된다고 여긴 것이다. 하지만 이건 큰 착각이었다. 로그인은 이 사람이 계정의 주인이 맞다는 것만 확인해 줄 뿐, 그 사람이 지금 하려는 특정 행동을 해도 되는지는 전혀 보장하지 않는다. 신분증을 확인하는 것과, 그 사람에게 특정 방에 들어갈 자격이 있는지 확인하는 것은 완전히 다른 문제다.
구체적인 위험을 그려 보면 분명하다. 어떤 사용자가 자기 글을 수정하는 기능을 쓴다고 하자. 만약 서버가 이 요청을 처리할 때 어느 글을 수정할지를 사용자가 보낸 값에만 의존하고, 그 글이 정말 이 사용자의 것인지 확인하지 않으면 어떻게 될까. 공격자는 값을 바꿔 남의 글 번호를 넣어 볼 수 있다. 그러면 남의 글을 자기 글처럼 고치거나 지우게 된다. 로그인은 했지만 권한 확인이 빠진 전형적인 구멍이다.
이런 유형의 허점은 겉으로 잘 드러나지 않아 더 위험하다. 화면에는 자기 글의 수정 버튼만 보이니 정상 사용자는 남의 글을 건드릴 일이 없다. 하지만 앞 편들에서 거듭 말했듯 공격자는 화면을 거치지 않고 서버에 직접 요청을 보낸다. 화면에 버튼이 없다고 그 기능이 막힌 게 아니다. 그래서 권한 확인은 반드시 서버에서, 요청을 실제로 처리하기 직전에 이뤄져야 했다.
정리하면 인증과 권한은 순서가 있는 두 관문이다. 먼저 인증으로 누구인지 확인하고, 그다음 권한으로 이 사람이 이 자원에 이 행동을 할 자격이 있는지 확인한다. 두 관문을 다 통과해야 요청이 처리된다. 인증만 있고 권한이 없으면, 로그인한 모든 사용자가 서로의 자원을 넘볼 수 있는 위험한 상태가 된다. 이 둘을 분리해 생각하는 것이 권한 설계의 출발점이었다.
2. 역할로 큰 틀을 나누기
권한을 다루는 첫 번째 축은 역할이었다. 우리 사이트에는 크게 몇 가지 부류의 사용자가 있다. 평범한 일반 사용자, 사이트를 관리하는 운영자, 그리고 그 사이의 특별한 권한을 가진 계정들이다. 각 부류가 할 수 있는 일의 범위가 다르다. 운영자는 부적절한 글을 지우거나 사용자를 제재할 수 있지만, 일반 사용자는 그럴 수 없어야 한다.
그래서 사용자마다 어떤 역할인지를 기록해 두고, 민감한 기능을 처리할 때 그 역할을 확인했다. 운영자만 쓸 수 있는 기능이라면, 요청한 사용자의 역할이 운영자인지 서버에서 검사한 뒤에야 실행한다. 역할이 맞지 않으면 요청을 거절한다. 이렇게 역할로 큰 틀을 나누면, 사용자 부류에 따른 권한을 체계적으로 관리할 수 있었다.
역할 확인은 자주 일어나는 작업이라 성능도 신경 써야 했다. 매 요청마다 사용자의 역할을 무겁게 조회하면 부담이 되니, 역할 정보를 짧은 시간 동안 가볍게 기억해 두고 재활용하는 식으로 비용을 줄였다. 다만 이때도 역할이 바뀌었을 가능성을 고려해 기억을 너무 오래 붙들지는 않았다. 방어와 성능 사이의 균형은 여기서도 반복되는 과제였다.
역할을 설계할 때 지킨 원칙은 기본값을 최소 권한으로 두는 것이었다. 새 사용자나 애매한 경우에는 일단 가장 낮은 권한만 주고, 필요한 권한을 명시적으로 부여하는 방식이다. 반대로 기본을 넓게 열어 두고 위험한 것만 막으면, 하나라도 빠뜨리면 곧바로 구멍이 된다. 막을 것을 나열하기보다 허락할 것을 나열하는 게 안전하다는 원칙이, 앞서 입력 정화에서처럼 권한에서도 그대로 통했다.
3. 소유권을 매번 확인하기
역할이 큰 틀이라면, 더 세밀한 축은 소유권이었다. 같은 일반 사용자라도 자기 글은 고칠 수 있지만 남의 글은 고칠 수 없다. 역할만으로는 이걸 구분하지 못한다. 둘 다 일반 사용자니까. 그래서 특정 자원에 대한 행동은 그 자원의 주인이 요청자와 일치하는지를 따로 확인해야 했다. 이 자원이 정말 당신 것이 맞느냐를 묻는 확인이다.
우리 사이트는 글이나 댓글을 수정하거나 삭제할 때, 그 자원에 기록된 작성자와 지금 요청한 사용자가 같은지를 서버에서 대조했다. 같으면 허용하고 다르면 거절한다. 여기에 운영자는 예외적으로 남의 글도 관리할 수 있게 역할 조건을 더했다. 소유자이거나 운영자일 때만 통과시키는 이 조합으로, 자기 것은 자유롭게 다루되 남의 것은 함부로 못 건드리게 했다.
중요한 건 이 확인을 절대 사용자가 보낸 정보만으로 하지 않는 것이었다. 예를 들어 요청에 이 글은 내 것이라는 표시가 함께 왔다고 해서 그걸 믿으면 안 된다. 그 표시 자체를 공격자가 위조할 수 있기 때문이다. 반드시 서버가 신뢰할 수 있는 출처, 즉 서버의 저장소에 기록된 실제 소유자 정보를 기준으로 대조해야 했다. 사용자가 주장하는 것과 서버가 아는 사실을 구분하는 게 핵심이었다.
이 소유권 확인은 놓치기 쉬운 곳에 특히 신경 써야 했다. 글 수정처럼 눈에 띄는 기능은 확인을 잊지 않지만, 첨부 파일 접근이나 부가 기능처럼 덜 주목받는 경로에서 확인이 빠지곤 한다. 그래서 사용자 자원을 다루는 모든 경로를 훑어, 어디에서든 소유권 또는 역할 확인이 빠지지 않았는지 점검했다. 방어는 가장 약한 고리에서 뚫리니, 눈에 안 띄는 구석까지 살피는 게 중요했다.
4. 남의 권한을 빌리는 공격
권한을 이야기할 때 빼놓을 수 없는 공격이 하나 있다. 사용자의 권한을 본인 몰래 빌려 요청을 위조하는 공격이다. 원리는 이렇다. 사용자가 우리 사이트에 로그인해 있는 상태에서, 공격자가 만든 다른 페이지를 방문하게 유도한다. 그 페이지가 몰래 우리 사이트로 요청을 보내면, 브라우저는 사용자의 로그인 상태를 함께 실어 보낸다. 서버는 정상 사용자의 요청으로 착각하고 처리해 버린다.
이 공격의 교묘함은 사용자가 아무것도 안 했는데 그의 권한으로 행동이 일어난다는 데 있다. 예를 들어 사용자 몰래 그의 계정으로 글을 쓰거나 설정을 바꾸게 만들 수 있다. 사용자의 로그인 자체는 정당하니, 요청만 보면 진짜와 구분이 안 된다. 그래서 이 공격을 막으려면 이 요청이 정말 우리 사이트의 정상 화면에서 사용자가 의도해서 보낸 것인지를 추가로 확인하는 장치가 필요했다.
방어 방법은 여러 가지가 있었다. 요청이 정말 우리 사이트에서 시작됐는지 확인하는 표식을 두거나, 상태를 바꾸는 요청에는 공격자가 미리 알 수 없는 일회성 값을 함께 요구하는 방식이다. 그 값은 우리 정상 화면에서만 발급되니, 외부의 위조 페이지는 그 값을 알 수 없어 요청이 거부된다. 또 로그인 상태를 실어 나르는 방식 자체를 다른 출처의 요청에는 조심스럽게 다루도록 설정하는 것도 도움이 됐다.
이 대목에서 다시 확인한 건, 상태를 바꾸는 요청과 단순히 읽는 요청을 구분해 다뤄야 한다는 원칙이었다. 글을 읽는 것 같은 조회 요청은 위조돼도 피해가 적지만, 무언가를 쓰거나 바꾸거나 지우는 요청은 위조되면 실제 피해가 생긴다. 그래서 상태를 바꾸는 요청에 방어를 집중했다. 모든 요청을 똑같이 대하기보다, 위험한 요청을 골라 더 엄격히 지키는 선택과 집중이 효율적이었다.
5. 신뢰하지 않음을 기본으로
권한 설계를 관통하는 정신은 로그인한 사용자조차 무조건 신뢰하지 않는다는 것이었다. 인증을 통과했어도 매 행동마다 자격을 다시 확인한다. 이건 사용자를 의심해서가 아니라, 요청이 정말 그 사용자의 정당한 의도인지 보장할 수 없기 때문이다. 계정이 탈취됐을 수도, 요청이 위조됐을 수도 있으니, 확인을 거른 신뢰는 위험했다.
이 원칙은 시리즈 내내 반복된 신뢰하지 않고 확인한다는 태도의 정점이었다. 사용자를 인증으로 확인하고, 입력을 정화로 확인하고, 외부 자원을 지문으로 확인하고, 이제 권한까지 매번 확인한다. 편리함을 위해 어느 하나라도 그냥 믿어 버리면 거기서 구멍이 뚫렸다. 확인의 비용을 감수하는 것이 결국 사이트를 지키는 가장 확실한 길이었다.
지금까지 우리는 방어의 안팎을 두루 살폈다. 그런데 아무리 잘 막아도 사람 사이의 문제, 즉 스팸이나 악의적인 사용자가 만드는 유해한 콘텐츠는 남는다. 기술적 방어만으로는 부족하고, 사용자와 운영자가 함께 유해한 것을 걸러 내는 장치가 필요하다. 그래서 다음 편에서는 신고와 자동 차단을 다루려 한다. 커뮤니티를 건강하게 유지하는 사회적 방어의 한 축을, 우리 사이트의 실제 운영과 함께 이야기하겠다.