쿠키의 자동 전송은 로그인 유지를 편리하게 하지만, 바로 그 편리함이 하나의 위험을 낳는다. 브라우저는 조건에 맞는 요청이면 사용자의 의도와 무관하게 인증 쿠키를 함께 실어 보내므로, 다른 사이트가 사용자의 로그인 상태를 몰래 빌려 요청을 일으킬 수 있다. 사이트 간 요청 위조는 이 틈을 파고드는 위협이고, 그에 대한 방어가 이번 편의 주제다.
이번 편은 이 위협이 어떤 원리로 성립하는지, 쿠키의 자동 전송이 왜 그 바탕이 되는지, 이를 막는 대표적인 방어들이 각각 어떻게 작동하는지, 그리고 그 방어들을 어떻게 겹쳐야 두꺼운 벽이 되는지를 다룬다. 위협을 이해하는 것은 그것을 미화하기 위해서가 아니라, 어디를 막아야 하는지를 알기 위해서다. 방어의 관점에서 이 틈의 구조를 정확히 읽는 것이 목표다.
위협이 성립하는 원리
이 위협은 사용자가 어느 사이트에 로그인한 상태에서 다른 사이트를 방문했을 때 성립한다. 그 다른 사이트가 원래 사이트로 향하는 요청을 만들어 두면, 사용자의 브라우저가 그 요청을 보내면서 원래 사이트의 인증 쿠키를 함께 싣는다. 원래 사이트는 그 쿠키를 보고 정당한 사용자의 요청으로 여겨 처리한다.
핵심은 사용자가 그 요청을 의도하지 않았다는 데 있다. 사용자는 다른 사이트를 둘러보고 있을 뿐인데, 그 사이트에 숨겨진 요청이 사용자의 이름으로 원래 사이트에 전달된다. 사용자의 신원과 인증은 진짜이지만, 요청을 일으킨 의도는 사용자의 것이 아니다. 신원은 맞고 의도는 없는 이 어긋남이 위협의 본질이다.
그래서 이 위협은 정보를 훔치는 것이 아니라 사용자의 이름으로 어떤 동작을 일으키는 데 목적이 있다. 상태를 바꾸는 동작, 예컨대 설정을 바꾸거나 무언가를 보내거나 지우는 동작이 사용자의 의도 없이 실행되게 만드는 것이다. 그래서 방어의 초점도 상태를 바꾸는 요청이 정말 사용자의 의도에서 나왔는지를 확인하는 데 놓인다.
이 위협은 사용자가 로그인한 상태를 전제로 하므로, 인증과 떼어 놓고 생각할 수 없다. 로그인 상태가 아니라면 빌려 쓸 인증도 없기 때문이다. 그래서 이 방어는 인증 설계의 한 부분으로 다루어지며, 인증 쿠키를 쓰는 이상 반드시 함께 세워야 하는 방어선이 된다.
자동 전송이라는 바탕
이 위협의 바탕에는 브라우저가 쿠키를 자동으로 싣는 성질이 있다. 브라우저는 요청의 대상이 맞으면, 그 요청이 어느 사이트에서 시작되었는지를 따지지 않고 저장된 쿠키를 함께 보낸다. 이 자동 전송이 로그인 유지를 편하게 하는 바로 그 성질이면서, 동시에 다른 사이트가 그 쿠키를 빌려 쓰게 하는 틈이 된다.
만약 인증 값이 자동으로 실리지 않고 요청마다 명시적으로 붙여야 하는 것이라면, 이 위협은 애초에 성립하기 어렵다. 다른 사이트는 원래 사이트의 인증 값을 알지 못하므로, 그것을 명시적으로 붙일 수 없기 때문이다. 자동으로 실리기에 값을 몰라도 빌려 쓸 수 있는 것이고, 명시적으로 붙여야 한다면 값을 모르는 한 붙일 수 없다.
여기서 방어의 두 갈래가 갈린다. 하나는 자동 전송 자체를 제한해 다른 사이트에서 시작된 요청에는 쿠키가 실리지 않게 하는 것이고, 다른 하나는 자동으로 실리지 않는 별도의 값을 요청에 요구해 다른 사이트가 그 값을 붙이지 못하게 하는 것이다. 앞의 것은 쿠키의 전송을 좁히고, 뒤의 것은 인증 외에 또 하나의 확인을 더한다.
이 두 갈래는 서로를 대체하기보다 보완한다. 어느 하나만으로도 상당 부분 막히지만, 둘을 겹치면 한쪽이 특정 상황에서 약해지더라도 다른 쪽이 버틴다. 그래서 실제 방어는 이 둘을 함께 세우는 방향으로 이루어진다. 각 방어가 무엇을 하는지 차례로 살펴본다.
전송을 좁히는 방어
첫 번째 방어는 쿠키가 다른 사이트에서 시작된 요청에 실리지 않도록 전송을 좁히는 것이다. 쿠키에는 어떤 출처의 요청에 실릴지를 제한하는 속성이 있어, 이를 엄격하게 설정하면 그 쿠키를 심은 사이트 자체에서 시작된 요청에만 쿠키가 실린다. 다른 사이트에서 건너온 요청에는 인증 쿠키가 붙지 않으므로, 그 요청은 인증되지 않은 것으로 처리된다.
이 방어는 위협의 바탕인 자동 전송을 직접 겨냥한다. 다른 사이트가 요청을 만들어도 그 요청에 인증 쿠키가 실리지 않으면, 원래 사이트는 그것을 정당한 사용자의 요청으로 여기지 않는다. 위협이 성립하는 조건 자체를 없애는 근본적인 방어다.
다만 이 속성을 지나치게 엄격하게 두면 정상적인 외부 진입까지 막힐 수 있다. 다른 사이트의 링크를 눌러 원래 사이트로 들어오는 경우처럼 사용자가 의도한 이동에서도 쿠키가 실리지 않아 로그인이 풀린 것처럼 보일 수 있다. 그래서 이 속성에는 완전히 막는 단계와 안전한 이동은 허용하는 중간 단계가 있으며, 쿠키의 쓰임에 맞춰 적절한 단계를 고른다.
이 방어는 강력하지만 그것 하나에만 기대기는 어렵다. 이 속성이 모든 환경에서 똑같이 엄격하게 지켜진다고 장담하기 어렵고, 안전한 이동을 허용하는 중간 단계를 쓸 때는 그 틈으로 일부 요청이 지날 수 있기 때문이다. 그래서 이 방어는 반드시 두되, 그것만으로 충분하다고 여기지 않고 다른 방어를 겹친다.
확인 값을 요구하는 방어
두 번째 방어는 상태를 바꾸는 요청에 자동으로 실리지 않는 확인 값을 함께 요구하는 것이다. 원래 사이트가 사용자에게 미리 넘겨준 값을 요청에 담게 하고, 그 값이 맞을 때만 요청을 처리한다. 다른 사이트는 이 값을 알지 못하므로, 요청을 만들어도 그 값을 담지 못해 처리되지 않는다.
이 방어의 핵심은 확인 값이 쿠키처럼 자동으로 실리지 않는다는 데 있다. 인증 쿠키는 브라우저가 알아서 싣지만, 이 확인 값은 원래 사이트의 화면을 통해 받아 요청에 명시적으로 담아야 한다. 다른 사이트는 원래 사이트의 화면에 접근해 그 값을 읽을 수 없으므로, 값을 담을 방법이 없다. 값을 모르면 붙일 수 없다는 성질이 방어를 만든다.
이 확인 값은 예측할 수 없어야 하고, 사용자마다 다르며, 적절히 짧은 수명을 가져야 한다. 값을 짐작할 수 있거나 여러 사용자에게 같으면, 다른 사이트가 그 값을 미리 맞춰 담을 여지가 생긴다. 그래서 값은 안전한 난수로 만들어 사용자의 세션에 묶어 두고, 요청에 담겨 돌아온 값을 그 세션의 값과 대조한다.
값을 세션에 묶어 대조하면, 다른 사용자의 값을 빌려 쓰는 것도 막힌다. 요청에 담긴 값이 그 요청을 보낸 사용자의 세션에 보관된 값과 일치해야 하므로, 남의 값을 담아도 세션이 다르면 걸러진다. 이 확인 값 방어는 자동 전송에 기대는 위협의 성립 조건, 곧 값을 몰라도 요청을 만들 수 있다는 조건을 정면으로 깨뜨린다.
출처를 확인하는 방어
세 번째로, 요청이 어디에서 시작되었는지를 나타내는 정보를 확인하는 방어가 있다. 브라우저는 상태를 바꾸는 일부 요청에 그 요청이 어느 출처에서 비롯되었는지를 함께 실어 보낸다. 원래 사이트는 이 출처 정보가 자신을 가리키는지 확인해, 다른 출처에서 시작된 요청을 걸러 낼 수 있다.
이 방어는 별도의 값을 미리 넘겨줄 필요 없이, 브라우저가 실어 주는 출처 정보만으로 판단한다는 점에서 간편하다. 출처가 원래 사이트가 아니면 그 요청은 다른 곳에서 시작된 것이므로 처리하지 않는다. 확인 값 방어가 값을 주고받아야 하는 데 비해, 이 방어는 요청에 딸려 오는 정보를 보기만 하면 된다.
다만 이 출처 정보가 언제나 실려 오는 것은 아니고, 환경에 따라 빠지거나 다르게 다루어질 수 있다. 정보가 없을 때 요청을 허용할지 막을지에 따라 방어의 강도와 호환성이 갈린다. 그래서 이 방어는 확인 값 방어를 보완하는 자리에 두는 경우가 많고, 그것만으로 전부를 막으려 하기보다 다른 방어와 함께 쓴다.
이 방어 역시 서버가 판단을 쥐고 있어야 한다. 출처 정보를 확인하는 판단을 서버에서 하고, 그 기준을 명확히 정해 두어야, 예상 밖의 출처가 슬그머니 통과하지 못한다. 어느 방어든 판단의 기준은 서버가 정하고 서버가 확인한다는 원칙이 여기서도 지켜진다.
안전한 메서드와 위험한 메서드의 구분
이 위협의 방어에서 중요한 구분이 상태를 바꾸는 요청과 그렇지 않은 요청이다. 조회처럼 상태를 바꾸지 않는 요청은 위조되어도 상태에 영향을 주지 않으므로 위협이 작다. 반면 무언가를 바꾸거나 보내거나 지우는 요청은 위조되면 실제 피해가 나므로, 방어의 초점이 여기에 놓인다.
그래서 규약을 결대로 따르는 설계가 방어에도 보탬이 된다. 조회에는 상태를 바꾸지 않는 메서드를 쓰고, 상태를 바꾸는 일에는 그에 맞는 메서드를 쓰면, 어떤 요청이 위험한지가 메서드만으로 구분된다. 상태를 바꾸는 메서드의 요청에만 확인 값을 요구하면, 방어를 위험한 요청에 집중할 수 있다.
반대로 조회 요청으로 상태를 바꾸는 설계는 이 구분을 무너뜨린다. 상태를 바꾸지 않아야 할 메서드로 상태를 바꾸면, 방어가 느슨한 조회 경로로 위조가 지나갈 수 있다. 그래서 메서드의 의미를 규약대로 지키는 것이 이 위협의 방어에서도 밑바탕이 된다. 규약을 지키는 설계가 방어의 기반을 함께 놓는 셈이다.
상태를 바꾸는 요청을 이렇게 구분해 두면, 방어를 어디에 두어야 하는지도 분명해진다. 확인 값을 요구하고 출처를 살피는 방어를 상태를 바꾸는 요청에 두고, 조회 요청은 상대적으로 가볍게 다룬다. 방어의 자원을 위험이 큰 곳에 집중하는 것이 효율적이면서도 빈틈이 적다.
방어를 겹쳐 두껍게 하기
지금까지 살펴본 방어들은 어느 하나만으로도 상당 부분 위협을 막지만, 각각 특정 상황에서 약해질 수 있다. 전송을 좁히는 속성은 중간 단계를 쓸 때 일부 요청이 지날 수 있고, 출처 정보는 없을 때가 있으며, 확인 값은 화면 설계에 따라 다루기가 번거롭다. 그래서 이들을 겹쳐 서로의 약한 지점을 메운다.
전송을 좁히는 속성을 기본으로 두어 자동 전송의 틈을 줄이고, 상태를 바꾸는 요청에 확인 값을 요구해 값을 모르는 요청을 걸러 내며, 출처 정보로 한 겹 더 확인한다. 한 방어가 특정 상황에서 통하지 않더라도, 다른 방어가 그 자리를 지킨다. 겹겹의 방어가 어느 하나의 실패를 전체의 실패로 번지지 않게 한다.
이 겹침은 인증 쿠키의 다른 보호와도 이어진다. 쿠키를 스크립트가 읽지 못하게 하고 암호화된 연결로만 오가게 하는 보호가 함께 있으면, 확인 값 방어를 우회하려는 다른 경로도 함께 막힌다. 여러 방어가 하나의 체계로 맞물릴 때, 특정 위협만이 아니라 그 주변의 우회로까지 닫힌다.
겹쳐 두더라도 각 방어가 제대로 작동하는지 확인하는 것을 잊지 말아야 한다. 방어를 여럿 두었다는 사실에 안심해 각각의 설정을 방치하면, 실제로는 어느 것도 온전히 작동하지 않을 수 있다. 그래서 상태를 바꾸는 요청이 확인 값 없이 통과하지 않는지, 다른 출처의 요청이 걸러지는지를 직접 확인해 방어가 살아 있음을 점검한다.
정리하면 사이트 간 요청 위조는 쿠키의 자동 전송을 빌려 사용자의 의도 없이 그 이름으로 상태를 바꾸는 위협이며, 방어는 쿠키의 전송을 좁히고, 자동으로 실리지 않는 확인 값을 요구하고, 요청의 출처를 살피는 것으로 이루어진다. 상태를 바꾸는 요청에 방어를 집중하고 규약대로 메서드를 쓰며, 이 방어들을 겹쳐 서로의 약한 지점을 메워야 어느 하나가 뚫려도 무너지지 않는 두꺼운 벽이 된다. 다음 편에서는 대량의 시도로 계정을 노리는 공격에 대한 방어를 들여다본다.