인증을 다룬 여러 편을 지나오며 세션과 토큰, 비밀번호와 두 번째 수단, 권한과 여러 방어를 살펴보았다. 이 마지막 편은 그 여정에서 되풀이해 마주친 실수들을 한자리에 모은다. 인증의 빈틈은 대개 새로운 것이 아니라, 이미 알려진 몇 가지 실수가 자리를 바꿔 다시 나타나는 것이다. 그 실수들을 한눈에 정리해 두면, 새로운 상황에서도 어디를 먼저 살펴야 할지가 분명해진다.
이번 편은 인증과 인가를 뒤섞는 실수, 클라이언트를 믿는 실수, 검증을 느슨하게 하는 실수, 값을 허술하게 다루는 실수, 무효화를 빠뜨리는 실수, 그리고 방어를 한 겹에만 기대는 실수를 차례로 짚는다. 앞선 편들에서 각각 다룬 내용을 실수의 관점에서 다시 엮어, 인증 설계의 점검 목록으로 삼는 것이 목표다.
인증과 인가를 뒤섞는 실수
가장 근본적인 실수는 누구인지를 확인하는 일과 무엇을 해도 되는지를 확인하는 일을 뒤섞는 것이다. 로그인만 되면 무엇이든 할 수 있게 두거나, 로그인 여부만 보고 그 사용자가 이 자원에 이 동작을 해도 되는지를 따지지 않으면, 정당한 사용자가 남의 자원까지 건드리는 빈틈이 열린다.
이 실수는 대개 인가를 별도의 관문으로 의식하지 않는 데서 온다. 로그인 화면을 잘 만들어 두면 보안이 끝났다고 여기고, 그 뒤에 각 요청이 정당한지를 따지는 관문을 소홀히 하는 것이다. 인증은 무대에 오를 자격일 뿐이고, 무대 위에서 무엇을 해도 되는지는 인가가 따로 정한다는 구분을 놓치면 이 실수가 반복된다.
방어는 매 요청마다 인증과 인가를 자원 단위로 다시 확인하는 것이다. 그냥 로그인했는지가 아니라, 이 사용자가 바로 이 자원에 이 동작을 해도 되는지를 그 자리에서 판정한다. 소유권 확인과 권한 검사를 처리의 맨 앞에 두고, 통과하지 못하면 실제 작업에 들어가기 전에 끊는다. 이 확인을 공통의 자리에 모아 모든 요청이 거치게 하면 빠뜨림이 줄어든다.
인가를 빠뜨린 자리는 정상 사용 흐름에서는 드러나지 않는다는 점도 기억해야 한다. 기능이 잘 돌아가는 것만 보고 안전하다고 믿기 쉬우므로, 권한 없는 사용자가 남의 자원에 접근을 시도하는 상황을 일부러 점검해야 이 빈틈이 드러난다. 방어자가 스스로 그런 시도를 재현해 보는 것이 이 실수를 찾아내는 길이다.
클라이언트를 믿는 실수
두 번째로 흔한 실수는 클라이언트에서 온 것을 그대로 믿는 것이다. 화면에서 버튼을 숨겼으니 안전하다고 여기거나, 요청에 담긴 신원과 권한 정보를 검증 없이 신뢰하는 것이 여기에 속한다. 클라이언트가 보낸 것은 모두 검증의 대상일 뿐 믿을 근거가 아니다.
화면에서 가리는 것은 인가가 아니라 눈속임이라는 점을 분명히 해야 한다. 버튼을 숨겨도 요청은 주소만 알면 직접 보낼 수 있으므로, 실제 방어는 서버가 요청을 처리하기 직전에 이루어져야 한다. 화면의 가림은 사용자의 편의를 위한 것이고, 그 위에 서버의 판단이 반드시 더해져야 한다.
요청에 담긴 신원이나 권한 주장을 믿는 것도 위험하다. 요청에 담긴 식별 번호를 그대로 신뢰하면 다른 번호를 넣어 남의 자원에 접근할 수 있고, 요청에 담긴 역할 정보를 믿으면 그것을 조작해 권한을 넘어설 수 있다. 사용자가 누구이고 무엇을 할 수 있는지는 서버가 자신이 쥔 정보로 판단해야 한다.
이 실수의 뿌리는 신뢰의 경계를 흐리는 데 있다. 클라이언트는 신뢰 밖에 있고 서버가 신뢰의 안쪽에 있다는 경계를 분명히 두면, 경계를 넘어온 모든 것을 검증하는 자세가 자연스러워진다. 누구인지도, 무엇을 해도 되는지도 서버가 최종적으로 판단해야 권한 상승의 문이 닫힌다.
검증을 느슨하게 하는 실수
세 번째 실수는 값을 받아들일 때 검증을 느슨하게 하는 것이다. 토큰을 만드는 것보다 받아들이는 것이 훨씬 더 신중해야 하는데, 서명만 보고 만료를 확인하지 않거나, 발급자와 대상을 대조하지 않거나, 토큰이 지시하는 대로 검증 방식을 따르는 것이 여기에 속한다.
토큰을 검증할 때는 서명이 유효한지, 만료되지 않았는지, 신뢰하는 발급자가 냈는지, 이 서비스를 위한 것인지를 모두 확인해야 한다. 이 가운데 하나라도 빠뜨리면 검증에 구멍이 생긴다. 특히 만료를 확인하지 않으면 수명을 짧게 잡은 의미가 사라지고, 발급자와 대상을 대조하지 않으면 엉뚱한 토큰이 받아들여진다.
검증의 기준은 토큰이 아니라 서버가 쥐고 있어야 한다. 토큰이 스스로 어떤 방식으로 검증하라고 지시하게 두면, 그 지시를 악용해 검증을 우회하려는 시도에 노출된다. 그래서 검증하는 서버는 자신이 기대하는 방식과 조건을 미리 정해 두고, 그에 맞는 값만 받아들인다. 통과 조건을 좁게 잡을수록 예상 밖의 값이 비집고 들어올 틈이 줄어든다.
이 실수는 편하게 받아들이려는 태도에서 비롯된다. 검증을 엄격히 하면 정당한 값까지 걸릴까 걱정해 조건을 느슨하게 두는 것이다. 그러나 인증에서는 의심하며 거부하는 것을 기본값으로 두는 편이 안전하다. 통과시킬 것을 좁게 정하고, 그에 맞지 않는 것은 거부하는 자세가 검증의 구멍을 막는다.
값을 허술하게 다루는 실수
네 번째 실수는 인증에 쓰이는 값을 허술하게 만들거나 보관하거나 전송하는 것이다. 추측 가능한 식별자를 쓰거나, 비밀번호를 원래 형태로 저장하거나, 비밀을 코드에 적어 두거나, 값을 암호화되지 않은 경로로 보내는 것이 여기에 속한다. 값 자체가 허술하면 그 위에 쌓은 방어가 무의미해진다.
식별자와 토큰 같은 값은 충분히 길고 예측 불가능한 무작위여야 한다. 순서대로 늘어나는 값이나 짧고 규칙적인 값은 다음 값을 짐작하거나 무작위로 맞혀 볼 여지를 준다. 값을 만드는 근원도 예측 가능한 규칙이 아니라 안전한 난수여야, 값을 훔치지 않는 한 가로챌 수 없다.
비밀번호와 대조에 쓰는 값은 되돌릴 수 없는 형태로, 솔트와 느린 계산을 갖춘 방식으로 저장한다. 서명에 쓰는 비밀 같은 값은 코드에서 분리해 안전한 저장소에 두고 접근을 좁힌다. 저장소가 언젠가 새어 나갈 수 있다는 전제 아래, 새어 나가도 값이 드러나지 않게 미리 대비하는 것이다.
값이 오가는 전송도 함께 지켜야 한다. 인증 값은 암호화된 연결로만 오가게 하고, 기록이나 주소에 값이 남지 않게 하며, 쿠키에는 스크립트 접근을 막고 암호화된 연결에서만 전송되게 하는 보호를 붙인다. 값을 잘 만들고도 전송과 보관에서 새어 나가면 소용이 없으므로, 값의 생애 전체를 지키는 자세가 필요하다.
무효화를 빠뜨리는 실수
다섯 번째 실수는 인증을 끊어야 할 때 제대로 끊지 못하는 것이다. 로그아웃을 클라이언트에서 값을 지우는 것으로 끝내거나, 비밀번호를 바꾼 뒤에도 기존 인증을 그대로 두거나, 만료를 검사하지 않아 기한이 지난 값이 계속 통하게 두는 것이 여기에 속한다.
로그아웃은 서버에서 인증을 무효로 만들고 클라이언트에서 값을 지우는 두 정리로 완성된다. 클라이언트에서 값을 지우는 것만으로는, 이미 새어 나간 값이 만료 전까지 그대로 통한다. 토큰 방식에서 즉시 무효화가 필요하다면 짧은 수명과 폐기 목록, 기준 시각 같은 서버 쪽 장치를 함께 두어야 한다.
비밀번호를 바꾸거나 도용이 의심되는 순간에는 그 사용자의 기존 인증을 모두 끊어야 한다. 한 세션만 끊고 나머지를 두면 도용된 접근이 그대로 살아남는다. 사용자 단위로 모든 인증을 무효로 만드는 수단을 갖추고, 권한이 줄어드는 변경에서도 관련 인증을 무효화해 새 상태가 즉시 반영되게 한다.
만료를 검사하지 않는 것도 무효화를 빠뜨리는 실수다. 세션이나 토큰에 만료를 두고도 검증할 때 확인하지 않으면 기한이 지난 것이 계속 통한다. 매 요청의 검증에 만료 확인을 반드시 넣고, 만료된 기록은 제때 정리한다. 무효화는 능동적으로 끊는 일과 만료로 자동으로 끊는 일을 모두 빠짐없이 다루어야 한다.
방어를 한 겹에만 기대는 실수
여섯 번째 실수는 하나의 방어에만 기대는 것이다. 두 번째 수단을 두었으니 비밀번호는 약해도 된다고 여기거나, 전송을 좁히는 속성 하나로 사이트 간 요청 위조가 완전히 막힌다고 믿거나, 하나의 방어를 세운 것으로 안심하는 것이 여기에 속한다. 어느 방어든 특정 상황에서 약해질 수 있으므로, 한 겹에만 기대면 그 겹이 뚫릴 때 전체가 무너진다.
인증의 방어는 여러 겹을 겹쳐 서로의 약한 지점을 메울 때 두꺼워진다. 비밀번호를 안전하게 저장하면서 두 번째 수단을 더하고 시도의 빈도를 제한하면, 저장 값이 새어 나가도 두 번째 수단이 버티고 대량 시도도 걸린다. 사이트 간 요청 위조도 전송을 좁히는 속성과 확인 값과 출처 확인을 겹쳐야 한 방어의 빈틈을 다른 방어가 메운다.
겹치되 각 방어가 저마다 튼튼해야 한다는 점도 잊지 말아야 한다. 다른 방어가 있으니 이 방어는 허술해도 된다는 태도는, 겹침을 명분으로 각 겹을 얇게 만드는 것이다. 각 방어는 그 자체로 튼튼하고, 겹침은 그 위에 더해지는 것이라야, 여러 겹이 실제로 두꺼운 벽을 이룬다.
방어를 여럿 두었다는 사실에 안심해 각각을 점검하지 않는 것도 경계해야 한다. 여러 방어를 세워 두고 방치하면, 실제로는 어느 것도 온전히 작동하지 않을 수 있다. 그래서 상태를 바꾸는 요청이 확인 없이 통과하지 않는지, 만료된 값이 걸러지는지, 인증 값이 안전하게 오가는지를 직접 확인해 방어가 살아 있음을 점검한다.
실수를 찾아내는 자세
이 실수들은 대부분 정상 사용 흐름에서는 드러나지 않는다. 기능이 잘 돌아가는 것만 보아서는 인가의 빈틈도, 검증의 구멍도, 무효화의 누락도 보이지 않는다. 그래서 방어를 점검할 때는 정상 흐름만이 아니라, 정당하지 않은 시도가 어떻게 처리되는지를 함께 살펴야 한다.
정당하지 않은 시도를 스스로 재현해 보는 것이 이 실수들을 찾아내는 길이다. 남의 식별 번호를 넣어 보고, 권한 없는 주소를 두드려 보고, 만료된 값이나 조건이 맞지 않는 값을 보내 보고, 로그아웃한 뒤에도 옛 값이 통하는지 확인해 본다. 방어자가 이런 시도를 해 보면, 정상 흐름에서는 결코 드러나지 않던 빈틈이 모습을 드러낸다.
실수를 줄이는 또 하나의 길은 검증된 것을 그대로 쓰는 것이다. 비밀번호 저장 방식이나 인증 흐름의 구현을 직접 만들다 보면, 놓치기 쉬운 방어를 빠뜨리게 된다. 이미 검증된 것을 쓰되 그것이 무엇을 하고 있는지를 이해하면, 직접 만들다 저지르는 실수를 피하면서도 그 방어를 제대로 다룰 수 있다.
무엇보다 인증은 한 번 세워 두고 잊는 것이 아니라 꾸준히 살펴야 하는 것임을 잊지 말아야 한다. 새 기능이 붙으며 새 빈틈이 생기고, 시간이 지나며 옛 방어가 약해진다. 그래서 인증의 방어는 값의 생애와 요청의 흐름을 따라가며 정기적으로 점검하고, 새로 알려진 위협에 맞추어 다듬어야 한다. 이 꾸준함이 이미 알려진 실수가 자리를 바꿔 다시 나타나는 것을 막는다.
정리하면 인증의 흔한 실수는 인증과 인가를 뒤섞고, 클라이언트를 믿고, 검증을 느슨하게 하고, 값을 허술하게 다루고, 무효화를 빠뜨리고, 방어를 한 겹에만 기대는 것으로 요약된다. 이 실수들은 대개 정상 흐름에서는 드러나지 않으므로, 정당하지 않은 시도를 스스로 재현해 찾아내고, 검증된 것을 이해하며 쓰고, 꾸준히 점검해야 한다. 이 시리즈가 세션의 상태 관리에서 출발해 토큰과 비밀번호, 여러 인증 수단과 권한, 그리고 겹겹의 방어까지 한 바퀴를 돌며 세운 것은, 새로운 도구가 아무리 바뀌어도 그 아래에서 인증이 무엇을 지키려 하는지를 읽어 내는 눈이다. 도구는 낡아도 그 눈은 낡지 않는다.