로그인을 한 번 하면 그다음 페이지들이 나를 알아본다. 무상태인 HTTP에서는 서버가 앞선 요청을 기억하지 않는데도 이런 일이 가능한 이유가 무엇인지가 이번 편의 출발점이다. 답은 서버가 기억하는 것이 아니라, 매 요청마다 내가 누구인지를 증명하는 정보가 함께 실려 간다는 데 있다. 그 정보를 실어 나르는 대표적인 수단이 쿠키다.


쿠키와 세션은 무상태라는 성질의 빈틈을 메우기 위해 고안된 장치다. 서버가 상태를 기억하지 않으니, 상태를 이어 가려면 그 상태를 클라이언트가 지니고 다니거나 서버 바깥의 저장소에 두어야 한다. 이번 편은 쿠키가 어떻게 동작하는지, 세션과 토큰은 무엇이 다른지, 그리고 이 수단들이 안고 있는 보안 위협과 그 방어를 다룬다.

무상태를 메우는 쿠키

쿠키는 서버가 클라이언트에게 맡겨 두는 작은 데이터 조각이다. 서버가 응답에 쿠키를 담아 보내면 브라우저는 그것을 저장해 두었다가, 같은 서버로 요청을 보낼 때마다 그 쿠키를 함께 실어 보낸다. 이 왕복 덕분에 서버는 매 요청에서 앞서 맡겨 둔 정보를 되받아 볼 수 있다.


이 구조가 무상태를 메우는 방식이다. 서버는 여전히 아무것도 기억하지 않지만, 클라이언트가 대신 정보를 지니고 다니며 매번 되돌려 주므로 상태가 이어지는 것처럼 보인다. 로그인 이후에 서버가 나를 알아보는 것은, 로그인할 때 받아 둔 쿠키를 브라우저가 매 요청에 붙여 보내기 때문이다.


쿠키에 담기는 것은 대개 사용자를 식별하는 값이다. 사용자 정보 전체를 담기보다, 그 사용자를 가리키는 짧은 식별자를 담고 실제 정보는 서버가 따로 관리하는 방식이 흔하다. 쿠키가 클라이언트에 저장되는 만큼, 그 안에 민감한 정보를 그대로 담으면 위험하기 때문이다.


쿠키는 상태 유지 외에도 여러 용도로 쓰인다. 사용자의 화면 설정을 기억하거나, 방문 기록을 추적하거나, 장바구니의 내용을 유지하는 데도 쓰인다. 다만 용도가 무엇이든 쿠키는 매 요청에 실려 나가므로, 담는 정보가 많아지면 그만큼 모든 요청이 무거워진다는 점을 염두에 두어야 한다.


쿠키의 이런 성질을 이해하면 상태 유지의 본질이 보인다. 무상태인 규약 위에서 상태를 이어 가는 유일한 방법은 그 상태를 어딘가에 저장하고 매 요청에 결부시키는 것뿐이다. 그 저장 장소가 클라이언트냐 서버냐, 결부시키는 수단이 쿠키냐 다른 헤더냐에 따라 방식이 갈릴 뿐, 상태를 나른다는 근본 원리는 같다. 이 원리를 붙들고 있으면 새로운 인증 방식이 등장해도 그것이 어디에 상태를 두고 어떻게 요청에 싣는지를 기준으로 곧바로 파악할 수 있다.

쿠키가 설정되고 전송되는 흐름

쿠키의 생애는 서버의 응답에서 시작된다. 서버가 응답 헤더에 쿠키를 설정하는 지시를 담아 보내면, 브라우저는 그 지시를 읽어 쿠키를 저장한다. 이 설정 지시에는 쿠키의 이름과 값뿐 아니라, 언제까지 유효한지 어느 경로에서 쓸지 같은 조건도 함께 담긴다.


저장된 쿠키는 그 조건에 맞는 요청마다 자동으로 실려 나간다. 브라우저는 요청을 보낼 때 그 목적지에 해당하는 쿠키를 골라 요청 헤더에 담는다. 개발자가 매번 쿠키를 직접 붙일 필요가 없다는 점이 특징이다. 이 자동 전송이 편리함의 원천이자, 동시에 뒤에서 다룰 보안 위협의 원천이기도 하다.


쿠키에는 유효 기간을 정할 수 있다. 기간을 정하지 않으면 브라우저를 닫을 때 사라지는 임시 쿠키가 되고, 기간을 정하면 그때까지 저장되는 지속 쿠키가 된다. 로그인 상태를 브라우저를 닫아도 유지하려면 지속 쿠키를 쓰고, 그 세션에서만 유지하려면 임시 쿠키를 쓴다. 유효 기간의 설정이 사용자 경험을 좌우한다.


쿠키가 언제 전송될지는 그 범위 설정에 달렸다. 어느 도메인과 어느 경로에서 이 쿠키를 쓸지를 지정하면, 브라우저는 그 범위에 맞는 요청에만 쿠키를 붙인다. 범위를 넓게 잡으면 여러 하위 영역에서 쿠키가 공유되고, 좁게 잡으면 특정 경로에서만 쓰인다. 이 범위를 잘못 잡으면 쿠키가 필요 없는 요청에까지 실려 나가거나, 정작 필요한 곳에 닿지 못한다.

쿠키의 속성들

쿠키에는 그 동작과 안전성을 조정하는 여러 속성이 붙는다. 이 속성들을 어떻게 설정하느냐가 쿠키의 보안 수준을 결정하므로, 각 속성의 의미를 정확히 아는 것이 중요하다. 속성을 소홀히 다룬 쿠키는 공격의 통로가 되기 쉽다.


스크립트의 접근을 막는 속성이 있다. 이 속성을 붙이면 페이지의 스크립트가 쿠키의 값을 읽지 못한다. 악성 스크립트가 삽입되었을 때 로그인 정보를 담은 쿠키를 훔쳐 가는 것을 막는 장치다. 사용자를 식별하는 중요한 쿠키에는 이 속성을 반드시 붙이는 것이 원칙이다.


암호화된 연결에서만 전송되도록 하는 속성도 있다. 이 속성을 붙인 쿠키는 안전한 연결이 아니면 실려 나가지 않는다. 암호화되지 않은 통로로 쿠키가 새어 나가 중간에서 가로채이는 것을 막는다. 로그인 관련 쿠키라면 이 속성 역시 기본으로 갖추어야 한다.


다른 사이트에서 시작된 요청에 이 쿠키를 실을지를 정하는 속성도 중요하다. 이 속성을 엄격하게 설정하면, 우리 사이트가 아닌 다른 곳에서 시작된 요청에는 쿠키가 붙지 않는다. 뒤에서 다룰 요청 위조 공격을 막는 핵심 수단이다. 이 속성의 기본값이 점점 안전한 쪽으로 바뀌어 온 것도 그만큼 위협이 컸기 때문이다.

세션이라는 방식

세션은 상태를 서버 쪽에 두는 방식이다. 서버가 각 사용자마다 정보 묶음을 만들어 저장소에 보관하고, 그것을 가리키는 짧은 식별자만 쿠키로 클라이언트에 맡긴다. 클라이언트는 그 식별자만 지니고 다니고, 실제 정보는 서버가 관리한다. 이렇게 하면 민감한 정보가 클라이언트에 노출되지 않는다.


세션 방식의 장점은 통제력이다. 정보가 서버에 있으므로, 특정 사용자의 세션을 서버가 언제든 무효화할 수 있다. 로그아웃을 시키거나 의심스러운 접속을 강제로 끊는 일이 서버 쪽에서 간단히 이루어진다. 상태의 열쇠를 서버가 쥐고 있다는 점이 이 방식의 힘이다.


단점은 서버가 상태를 보관해야 한다는 데서 나온다. 사용자가 늘수록 저장해야 할 세션도 늘고, 여러 대의 서버가 있다면 그 세션을 어떻게 공유할지가 문제가 된다. 한 서버에만 세션을 두면 다른 서버로 간 요청이 그 세션을 찾지 못하므로, 세션을 공유 저장소에 두는 구조가 필요해진다. 무상태의 이점을 일부 되돌려 놓는 셈이다.


그럼에도 세션은 오랫동안 검증된 방식이다. 통제가 명확하고 구조가 직관적이어서, 서버가 상태를 관리하는 부담을 감당할 수 있다면 여전히 견고한 선택이다. 저장소를 공유하고 만료를 잘 관리하면 여러 서버 환경에서도 문제없이 동작한다. 오래된 방식이라고 낡은 것은 아니다.

토큰이라는 대안

세션과 다른 방식으로, 상태를 클라이언트가 지닌 토큰에 담는 방법이 있다. 이 방식에서는 서버가 사용자의 정보를 담아 서명한 토큰을 발급하고, 클라이언트는 그 토큰을 매 요청에 실어 보낸다. 서버는 토큰의 서명을 확인해 그 내용이 위조되지 않았음을 검증하고, 담긴 정보를 그대로 신뢰한다.


이 방식의 이점은 서버가 상태를 저장하지 않아도 된다는 데 있다. 필요한 정보가 토큰 안에 다 들어 있으므로, 서버는 토큰만 검증하면 되고 별도의 저장소를 조회할 필요가 없다. 여러 서버가 같은 서명 열쇠만 공유하면 어느 서버든 토큰을 검증할 수 있어, 무상태의 확장성과 잘 어울린다.


단점은 통제가 어렵다는 점이다. 토큰은 한 번 발급되면 그 안에 담긴 유효 기간까지 서버가 저장하지 않은 채로도 유효하다. 그래서 특정 토큰을 즉시 무효화하기가 까다롭다. 유출된 토큰을 강제로 끊으려면 별도의 폐기 목록을 두어야 하는데, 그러면 무상태의 이점이 일부 사라진다. 유효 기간을 짧게 두어 위험을 줄이는 것이 흔한 절충이다.


토큰 방식은 서로 다른 서비스가 인증을 주고받는 구조에서 특히 유용하다. 한 곳에서 발급한 토큰을 다른 곳이 검증할 수 있으므로, 여러 서비스가 인증을 공유하기 쉽다. 이런 특성 때문에 최근의 많은 시스템이 토큰 방식을 택한다. 다만 편리함의 이면에 통제의 어려움이 있다는 점을 잊지 말아야 한다.

쿠키의 보안 위협

쿠키가 자동으로 전송된다는 편리함은 그대로 위협이 된다. 대표적인 것이 요청 위조 공격이다. 공격자가 사용자를 속여 우리 사이트로 요청을 보내게 만들면, 브라우저는 그 요청에 우리 쿠키를 자동으로 붙여 버린다. 사용자가 의도하지 않은 요청이 사용자의 권한으로 처리되는 것이다.


이 공격을 막는 방법이 앞서 말한 다른 사이트 요청에 쿠키를 싣지 않는 속성이다. 이 속성을 엄격히 설정하면 외부에서 시작된 요청에 쿠키가 붙지 않으므로 공격이 무력해진다. 여기에 더해, 상태를 바꾸는 요청에는 그 요청이 정말 우리 페이지에서 시작되었는지를 확인하는 별도의 토큰을 요구하는 방어도 함께 쓴다.


또 하나의 위협은 악성 스크립트가 쿠키를 훔치는 것이다. 페이지에 삽입된 악성 스크립트가 쿠키를 읽어 외부로 보내면 로그인 정보가 통째로 넘어간다. 이 위협은 스크립트의 접근을 막는 속성으로 상당 부분 차단된다. 물론 근본적으로는 악성 스크립트가 삽입되지 않도록 입력을 검증하고 출력을 처리하는 것이 우선이다.


중간에서 통신을 가로채 쿠키를 빼내는 위협도 있다. 암호화되지 않은 연결로 쿠키가 오가면 중간의 누군가가 그것을 엿볼 수 있다. 이 위협은 암호화된 연결에서만 쿠키를 전송하는 속성과, 사이트 전체를 암호화된 연결로 강제하는 정책으로 막는다. 쿠키의 보안은 어느 한 속성이 아니라 여러 속성과 정책이 함께 작동해야 완성된다.

상태 유지 방식의 선택

세션과 토큰 중 무엇을 쓸지는 서비스의 성격에 달렸다. 사용자를 강하게 통제해야 하고 서버가 상태를 감당할 수 있다면 세션이 적합하다. 반면 서버를 자유롭게 늘려야 하고 여러 서비스가 인증을 공유해야 한다면 토큰이 어울린다. 두 방식은 우열의 문제가 아니라 상황에 맞추는 선택의 문제다.


어느 방식을 택하든 공통으로 지켜야 할 것은 쿠키의 보안 속성이다. 상태를 담은 쿠키에는 스크립트 접근을 막고, 암호화된 연결만 쓰게 하고, 다른 사이트 요청에 실리지 않게 하는 속성을 갖추어야 한다. 이 세 가지는 방식과 무관하게 기본으로 지켜야 하는 방어선이다.


상태 유지는 편의를 위해 무상태의 원칙에 예외를 두는 일이다. 그래서 꼭 필요한 만큼만 상태를 유지하고, 담는 정보를 최소로 줄이는 절제가 필요하다. 상태가 늘어날수록 관리와 보안의 부담도 함께 늘기 때문이다. 무엇을 기억하게 할지를 신중히 정하는 것이 안전한 설계의 시작이다.


상태 유지 방식을 결정할 때 흔히 저지르는 잘못은 유행을 기준으로 삼는 것이다. 토큰 방식이 널리 쓰인다는 이유만으로 통제가 중요한 서비스에까지 무비판적으로 도입하면, 정작 필요한 순간에 사용자의 접속을 끊지 못해 곤란해진다. 반대로 서버를 대규모로 늘려야 하는 구조에 세션을 고집하면 저장소 공유의 부담이 발목을 잡는다. 방식의 선택은 그 서비스가 통제와 확장 중 무엇을 더 중요하게 여기는지를 먼저 따진 뒤에 내려야 한다. 도구가 아니라 요구가 선택의 기준이 되어야 한다.


이번 편은 쿠키의 동작과 속성, 세션과 토큰의 차이, 그리고 쿠키를 둘러싼 보안 위협과 방어를 다루었다. 쿠키가 같은 출처 안에서 상태를 나른다면, 서로 다른 출처 사이의 요청은 또 다른 규칙의 지배를 받는다. 다음 편에서는 브라우저가 다른 출처로의 요청을 어떻게 통제하는지, 그 규칙의 이름인 교차 출처 자원 공유를 파고든다.