クッキーだけではリクエストの出どころは分からない
한국어 원문으로 표시합니다.
한 줄 요약
CSRF 는 브라우저가 쿠키를 자동으로 붙이는 성질을 빌려, 다른 사이트가 사용자 몰래 상태를 바꾸는 요청을 보내게 하는 공격이다. 쿠키만 보면 그 요청이 우리 페이지에서 시작됐는지 알 수 없으므로, 서버는 공격 페이지가 흉내 낼 수 없는 신호 를 따로 요구해야 한다.
왜 이게 필요했나
사용자가 bank.example.com 에 로그인해 둔 채 다른 탭에서 공격자의 페이지를 연다. 그 페이지에는 bank.example.com/transfer 로 향하는 폼이 숨어 있고 열리자마자 제출된다. 브라우저는 목적지가 은행이니 은행 쿠키를 붙인다. 서버에 도착한 요청은 사용자가 직접 누른 이체와 한 글자도 다르지 않다.
공격자는 응답을 읽지 못한다. 교차 출처 응답은 동일 출처 정책이 가린다. 그래서 CSRF 는 훔치는 공격이 아니라 쓰는 공격이고, 방어도 "이 쓰기가 정말 우리 페이지에서 왔는가" 를 확인하는 방향으로 발전했다.
어떻게 동작하나
OWASP CSRF 방지 치트시트 가 정리한 방어는 네 갈래다.
동기화 토큰(synchronizer token). 서버가 세션마다 예측할 수 없는 값을 만들어 세션에 저장하고 페이지에 내려준다. 상태를 바꾸는 요청은 그 값을 헤더나 숨은 필드로 돌려보내야 한다. 공격 페이지는 동일 출처 정책 때문에 그 값을 읽을 수 없다. OWASP 는 토큰을 쿠키로 보내지 말고, 폼 필드보다 커스텀 헤더를 쓰라고 권한다. 여기서 흔히 빠뜨리는 것이 세션과의 결속이다. "우리가 발급한 토큰인가" 만 보면 공격자가 자기 계정으로 받은 진짜 토큰을 피해자 요청에 넣어도 통과한다. 비교는 시간이 새지 않도록 hmac.compare_digest 같은 상수 시간 함수로 한다.
서명된 이중 제출 쿠키. 서버에 상태를 두기 싫을 때 쓴다. 무작위 값과 세션 ID 를 묶어 HMAC 으로 서명한 값을 쿠키와 헤더에 함께 싣는다. 서명 없이 쿠키와 헤더가 같은지만 보는 방식은 OWASP 가 권하지 않는다. 하위 도메인이나 네트워크 위치에서 쿠키를 주입할 수 있는 공격자는 두 값을 똑같이 맞출 수 있기 때문이다.
Fetch Metadata 와 Origin. 브라우저는 요청마다 Sec-Fetch-Site 를 붙인다. 값은 same-origin·same-site·cross-site·none 이고, Sec- 로 시작하는 금지 헤더라 페이지 스크립트가 바꿀 수 없다. OWASP 는 cross-site 인 POST·PUT·PATCH·DELETE 를 거절하고, same-site 는 형제 서브도메인을 믿을 때만 허용하라고 한다. 헤더가 없는 옛 클라이언트는 막거나(민감한 엔드포인트에 권장), Origin 검사와 토큰으로 넘겨 판단한다. Origin 헤더는 허용 목록과 문자열 전체가 같은지 본다. 앞부분만 비교하면 http://127.0.0.1:8303.evil.example 같은 이름이 통과한다.
커스텀 헤더. 교차 출처 요청에 커스텀 헤더를 붙이려면 CORS 사전 요청을 통과해야 한다. 서버가 허용하지 않으면 요청이 나가지 않는다. 그래서 CORS 를 느슨하게 열면 이 방어도 같이 무너진다.
SameSite 는 보조다. RFC 6265bis 초안 에서 Lax 쿠키는 같은 사이트 요청과, 교차 사이트라도 안전한 메서드의 최상위 이동에는 실린다. 한계가 셋 있다.
same-site ≠ same-origin blog.example.com 과 bank.example.com 은 같은 사이트다
GET 으로 상태 변경 링크 하나로 최상위 GET 이동이 되고 Lax 쿠키가 실린다
기본값의 예외 속성이 없으면 Lax 와 같은 기본 모드지만 브라우저가
Lax-allowing-unsafe 를 쓸 수 있다
마지막 줄에 대해 초안은 그런 브라우저가 최근 만든 쿠키에만 예외를 두라고 하며 2분 이하를 합리적 한계로 든다. web.dev 에 따르면 Chrome 은 80 부터 속성 없는 쿠키를 Lax 로 다루되, 최상위 POST 에 약 2분 동안 쿠키를 허용하는 임시 완화를 두었다. 명시적 SameSite=Lax 에는 이 예외가 없다. OWASP 결론도 같다 — SameSite 는 심층 방어일 뿐 CSRF 방어를 대신하지 못한다.
로그인 폼도 대상이다. 로그인 CSRF 는 피해자를 공격자 계정으로 로그인시켜, 피해자가 입력한 것이 공격자 계정에 쌓이게 한다. 로그인 전에는 세션이 없어 토큰을 묶을 곳이 없으니, OWASP 는 로그인 전 세션을 만들어 토큰을 싣고 로그인 뒤에는 세션을 새로 만들라고 한다.
현장에서 만나는 모습
GET /logout, GET /approve?id=42 처럼 GET 으로 상태를 바꾸는 엔드포인트가 남아 있으면 SameSite=Lax 는 아무것도 막지 못한다. 링크 하나면 쿠키가 실린다. OWASP 가 "상태를 바꾸는 작업에 GET 을 쓰지 말라" 고 따로 적은 이유다.
회사 서비스가 한 등록 도메인 아래 여러 서브도메인으로 흩어져 있는 경우도 흔하다. 캠페인 페이지 하나에 XSS 가 생기면 그 페이지는 은행 서브도메인에 same-site 요청을 보낸다. SameSite 는 통과하고, 형제를 믿지 않는 Fetch Metadata 정책과 세션에 묶인 토큰만 막아 준다.
다음 실습에서 할 것
127.0.0.1:8303 에 로그인되는 이체 서버를 세우고 세션별 CSRF 토큰을 발급한다. 채점기가 curl 로 위조 요청을 직접 만든다. 토큰 없는 요청, 남의 세션 토큰, Sec-Fetch-Site: cross-site, 허용 목록 밖의 Origin 을 보내 모두 403 이고 이체가 기록되지 않았는지 까지 확인한다. 마지막으로 SameSite=Lax 쿠키가 어떤 교차 사이트 요청에 실리는지 판정표를 채운다.