크로스 사이트 요청 위조
로그인해 둔 사용자의 브라우저가 공격자가 시킨 요청을 대신 보내게 되는 일입니다. 브라우저는 그 사이트에 딸린 쿠키를 요청에 자동으로 붙입니다. 서버는 그것을 본인이 시킨 일로 봅니다. 그래서 그대로 실행합니다.
상세
CSRF(Cross-Site Request Forgery, 크로스 사이트 요청 위조)에서 공격자는 대상 사이트의 코드를 훔쳐보거나 뚫지 않습니다. MDN(Mozilla Developer Network) 용어집은 공격자가 악성 사이트에서 브라우저를 속여 대상 사이트로 HTTP(HyperText Transfer Protocol) 요청을 내게 만든다고 적습니다. 그 요청에는 사용자의 크리덴셜이 함께 실립니다. 서버는 사용자가 그걸 의도했다고 여깁니다. 그리고 해로운 동작을 수행합니다.
OWASP(Open Worldwide Application Security Project) 는 이것을 최종 사용자가 현재 인증되어 있는 웹 애플리케이션에서 원치 않는 동작을 실행하도록 강제하는 공격이라고 적습니다. 메일이나 채팅으로 링크를 보내는 정도의 사회공학이 곁들여지면, 공격자가 고른 동작을 사용자가 실행하게 만들 수 있습니다. 피해자가 일반 사용자면 공격이 성공했을 때 송금이나 이메일 주소 변경 같은 상태 변경 요청을 강제당할 수 있습니다. 피해자가 관리자 계정이면 웹 애플리케이션 전체가 넘어갈 수 있습니다.
서버가 이걸 걸러 내기 어려운 이유는 요청이 똑같이 생겼기 때문입니다. Spring Security 문서는 피해자의 사이트에서 온 HTTP 요청과 공격자의 사이트에서 온 요청이 정확히 같다고 적습니다. 그래서 악성 사이트에서 오는 요청만 거절하고 은행 사이트에서 오는 요청만 허용할 방법이 없다고 못 박습니다.
뿌리는 쿠키가 권한을 실어 나르는 방식에 있습니다. RFC(Request for Comments) 6265 는 쿠키로 사용자를 인증하는 서버가 보안 취약점을 겪을 수 있다고 적습니다. 일부 사용자 에이전트가 원격 측으로 하여금 그 사용자 에이전트에서 HTTP 요청을 내게 해 주기 때문입니다. 리다이렉트나 HTML(HyperText Markup Language) 폼이 그 수단으로 꼽힙니다. 그런 요청을 낼 때 사용자 에이전트는 원격 측이 쿠키의 내용을 모르더라도 쿠키를 붙입니다. 그래서 원격 측이 방심한 서버에서 권한을 행사하게 될 수 있습니다.
RFC 6265 는 이 보안 우려가 여러 이름으로 불린다고 적습니다. 그 예로 크로스 사이트 요청 위조와 confused deputy 를 듭니다. 그리고 문제의 뿌리가 쿠키라는 것이 ambient authority 의 한 형태라는 데 있다고 적습니다. 쿠키는 서버 운영자가 지시와 인가를 분리하도록 부추깁니다. 지시는 URL(Uniform Resource Locator) 의 형태로 옵니다. 인가는 쿠키의 형태로 옵니다. 그 결과 사용자 에이전트가 공격자가 지시한 리소스에 인가를 대신 실어 줄 수 있습니다. 그러면 서버나 그 클라이언트가 공격자가 지시한 동작을 사용자가 인가한 것처럼 수행하게 될 수 있습니다.
이름이 여럿입니다. OWASP 는 XSRF, "Sea Surf", Session Riding, Cross-Site Reference Forgery, Hostile Linking 을 함께 적습니다. 그리고 마이크로소프트가 위협 모델링 과정과 온라인 문서 여러 곳에서 이 공격을 One-Click attack 이라고 부른다고 적습니다.
발생 조건
MDN 용어집은 웹사이트가 다음 셋을 만족할 때 CSRF 공격이 가능하다고 적습니다.
- 서버의 상태를 바꾸는 데 HTTP 요청을 쓴다
- 요청이 인증된 사용자에게서 왔는지 확인하는 데 쿠키만 쓴다
- 요청에 공격자가 예측할 수 있는 파라미터만 쓴다
flowchart TD
A["요청이 서버 상태를 바꾸나"] -- 아니오 --> X["바꿀 상태가 없다"]
A -- 예 --> B["인증 확인을 쿠키만으로 하나"]
B -- 아니오 --> Y["쿠키만으로는 통과 못 한다"]
B -- 예 --> C["파라미터를 공격자가 예측하나"]
C -- 아니오 --> Z["요청을 완성 못 한다"]
C -- 예 --> D["위조된 요청이 실행된다"]
셋째 조건은 예측 못 하는 값 하나로 깨집니다. Django 는 POST 폼을 쓰는 템플릿이라면 <form>
안에 {% csrf_token %} 태그를 넣으라고 적습니다. 다만 외부 URL 을 대상으로 하는 POST 폼에는
넣지 말라고 적습니다. 그렇게 하면 CSRF 토큰이 유출되어 취약점으로 이어지기 때문입니다.
둘째 조건은 쿠키가 애초에 안 실리면 깨집니다. MDN 의 Set-Cookie 문서는 SameSite 속성이 쿠키를
크로스 사이트 요청과 함께 보낼지를 통제한다고 적습니다. 여기서 크로스 사이트란 쿠키를 설정한
사이트와 스킴까지 포함해 다른 사이트에서 시작된 요청을 말합니다. MDN 은 이 속성이 크로스 사이트
요청 위조를 포함한 일부 크로스 사이트 공격에 대해 어느 정도 보호를 제공한다고 적습니다.
Strict 는 쿠키를 설정한 그 사이트에서 시작된 요청에만 쿠키를 보냅니다. Lax 는 거기에 더해
두 조건을 함께 만족하는 크로스 사이트 요청에도 보냅니다. 최상위 내비게이션이어야 합니다.
그리고 안전한 메서드를 써야 합니다.
최상위 내비게이션은 브라우저 주소창에 보이는 URL 이 바뀌게 만드는 요청을 뜻합니다. fetch()
로 낸 요청, <img> 나 <script> 의 하위 리소스 요청, <iframe> 안의 내비게이션은 여기서
빠집니다. 최상위 브라우징 컨텍스트에서 사용자가 다른 사이트로 가는 링크를 누르는 것,
document.location 에 대입하는 것, <form> 제출은 여기에 들어갑니다. 안전한 메서드에서는
POST · PUT · DELETE 가 빠집니다. 어떤 브라우저들은 SameSite 가 안 적혀 있으면 Lax 를
기본값으로 씁니다. MDN 은 Lax 가 기본값으로 적용될 때는 더 관대한 판이 쓰인다고 덧붙입니다.
그 판에서는 요청 2분 전 이내에 설정된 쿠키라면 POST 요청에도 함께 실립니다.
첫째 조건이 빠지면 위조가 성공해도 서버에 남는 변화가 없습니다. 응답을 읽어 가는 것은 다른 문제입니다. 그 자리는 동일 출처 정책이 맡습니다.
예시
Spring Security 문서의 은행 송금 폼
Spring Security 문서는 은행 사이트가 현재 로그인한 사용자에게서 다른 계좌로 송금하는 폼을 제공한다고 가정합니다. 그 폼을 이렇게 적습니다.
<form method="post" action="/transfer">
<input type="text" name="amount"/>
<input type="text" name="routingNumber"/>
<input type="text" name="account"/>
<input type="submit" value="Transfer"/>
</form>
그에 대응하는 HTTP 요청은 이렇게 생겼습니다.
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
은행 사이트에 인증한 뒤 로그아웃하지 않은 채로 악성 사이트를 방문합니다. 그 사이트에는 이런 폼이 담긴 HTML 페이지가 있습니다.
<form method="post" action="https://bank.example.com/transfer">
<input type="hidden" name="amount" value="100.00"/>
<input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
<input type="hidden" name="account" value="evilsAccountNumber"/>
<input type="submit" value="Win Money!"/>
</form>
돈을 따고 싶어서 제출 버튼을 누릅니다. 그 과정에서 의도치 않게 악의적인 사용자에게 100달러를 송금하게 됩니다. 악성 사이트가 쿠키를 볼 수는 없지만, 은행에 딸린 쿠키는 여전히 요청과 함께 보내지기 때문입니다. Spring Security 문서는 이 전 과정이 자바스크립트로 자동화될 수도 있다고 적습니다. 그러면 버튼을 누를 필요조차 없습니다.
Rails 보안 가이드의 이미지 태그 한 줄
Rails 보안 가이드는 Bob 이라는 사용자로 같은 일을 보여 줍니다. Bob 이 게시판을 보다가 공격자의 글을 엽니다. 그 글에는 조작된 HTML 이미지 요소가 들어 있습니다. 이미지 파일이 아니라 Bob 의 프로젝트 관리 애플리케이션의 명령을 가리킵니다.
<img src="http://www.webapp.com/project/1/destroy">
Bob 은 몇 분 전에 로그아웃하지 않았으므로 www.webapp.com 의 세션이 아직 살아 있습니다. 글을
보는 것만으로 브라우저는 이미지 태그를 발견합니다. 그리고 www.webapp.com 에서 이미지로
짐작되는 것을 받아 오려 합니다. 그러면서 유효한 세션 ID 가 든 쿠키를 함께 보냅니다. 웹
애플리케이션은 해당 세션 해시의 사용자 정보를 확인합니다. 그리고 ID 가 1인 프로젝트를
삭제합니다. 그다음 결과 페이지를 돌려줍니다. 브라우저에게는 예상 밖의 결과라서 이미지를
표시하지 않습니다. Bob 은 공격을 알아채지 못합니다. 며칠 뒤에야 1번 프로젝트가 사라진 것을
알게 됩니다. Rails 가이드는 조작된 이미지나 링크가 반드시 그 웹 애플리케이션의 도메인에 있을
필요가 없다는 점을 짚습니다. 포럼이든 블로그 글이든 이메일이든 어디에나 있을 수 있습니다.
동작
sequenceDiagram
participant 브라우저
participant 대상 as 대상 사이트
participant 공격자 as 공격자 사이트
브라우저->>대상: 로그인
대상-->>브라우저: 세션 쿠키
브라우저->>공격자: 페이지 열기
공격자-->>브라우저: 대상 사이트로 가는 폼·이미지
브라우저->>대상: 요청 + 세션 쿠키 자동 첨부
대상-->>브라우저: 상태 변경 후 응답
다섯 단계입니다. 먼저 사용자가 대상 사이트에 인증합니다. 그 응답으로 세션 쿠키를 받습니다. Rails 가이드는 대부분의 Rails 애플리케이션이 쿠키 기반 세션을 쓴다고 적습니다. 세션 ID 는 쿠키에 담깁니다. 세션 해시는 서버 쪽에 남습니다. 아니면 세션 해시 전체가 클라이언트 쪽에 있습니다.
둘째로 사용자가 로그아웃하지 않은 채 공격자의 페이지를 엽니다. Spring Security 문서도 같은 자리를 전제로 둡니다. 은행 사이트에 인증한 뒤 로그아웃 없이 악성 사이트를 방문하는 대목입니다.
셋째로 그 페이지가 대상 사이트를 향한 요청을 브라우저에게 건넵니다. 숨긴 값으로 채운 폼이거나, 명령을 가리키는 이미지 태그입니다.
넷째로 브라우저가 그 요청을 냅니다. Rails 가이드는 브라우저가 어떤 도메인에 대한 쿠키를 찾을 수 있으면 그 도메인으로 가는 모든 요청에 쿠키를 자동으로 함께 보낸다고 적습니다. 그리고 논쟁적인 지점은 요청이 다른 도메인의 사이트에서 오더라도 쿠키를 보낸다는 것이라고 적습니다.
다섯째로 서버가 그 요청을 실행합니다. 서버 입장에서는 세션이 유효한 사용자의 요청이므로 프로젝트를 삭제하거나 송금을 처리합니다. 이 단계에서 상태가 실제로 바뀝니다.
경계
동일 출처 정책이 이걸 막아 주지 않나. 아닙니다.
MDN 은 동일 출처 정책이 서로 다른 두 오리진 사이의 상호작용을 통제한다고 적으면서, 그 상호작용을 세 부류로 나눕니다. 크로스 오리진 쓰기는 대체로 허용됩니다. 링크, 리다이렉트, 폼 제출이 그 예로 꼽힙니다. 크로스 오리진 임베딩도 대체로 허용됩니다. 크로스 오리진 읽기는 대체로 막힙니다. 다만 읽기 권한이 임베딩을 통해 새어 나가는 일이 잦습니다. CSRF 가 필요로 하는 것은 읽기가 아니라 쓰기이므로, 동일 출처 정책의 칸막이는 이 요청을 서지 못하게 하지 않습니다.
CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)도 같은 자리입니다. MDN 은 simple
request 라는 개념의 동기를 이렇게 적습니다. HTML 4.0 시절의 <form> 요소는 크로스 사이트
fetch() 와 XMLHttpRequest 보다 앞섭니다. 그리고 어느 오리진으로든 simple request 를 보낼
수 있습니다. 그래서 서버를 짜는 사람은 이미 크로스 사이트 요청 위조를 막고 있어야 합니다.
이 전제 위에서 서버는 폼 제출처럼 생긴 요청을 받는 데
프리플라이트로 옵트인할 필요가 없습니다. CSRF 의 위협이 폼 제출의 위협보다 더 나쁠 것이 없기
때문입니다. 다만 응답을 스크립트와 공유하려면 서버는 여전히 Access-Control-Allow-Origin
으로 옵트인해야 합니다. CORS 가 통제하는 것은 응답을 읽는 쪽이지 요청이 나가는 것이 아닙니다.
교차 사이트 스크립팅도 이건가. 아닙니다.
MDN 용어집은 교차 사이트 스크립팅을 공격자가 대상 사이트로 하여금 악성 코드를 마치 그 웹사이트의 일부인 것처럼 실행하게 만드는 공격이라고 적습니다. 그 코드는 사이트 자신의 코드가 할 수 있는 것은 무엇이든 할 수 있습니다. 실행 위치가 갈리는 선입니다. 교차 사이트 스크립팅은 공격자의 코드가 대상 사이트 안에서 돕니다. 크로스 사이트 요청 위조는 공격자의 코드가 바깥 사이트에 있습니다. 대상 사이트로 가는 것은 요청뿐입니다. 그래서 발생 조건의 셋째 칸이 갈립니다. 대상 사이트 안에서 도는 코드는 예측 못 하는 파라미터도 읽어 낼 수 있습니다. 그러면 CSRF 토큰이 방어선이 되지 못합니다.
관련 항목
크로스 사이트 요청 위조를 다루는 공식 문서
MDN · RFC 6265 · OWASP · Set-Cookie · Rails · Spring Security · Django
크로스 사이트 요청 위조와 헷갈리는 이웃
동일 출처 정책 · CORS · 교차 사이트 스크립팅 · 프리플라이트 · Access-Control-Allow-Origin
크로스 사이트 요청 위조가 비롯된 배경
쿠키 · 사용자 에이전트 · 권한 · 인증 · 애플리케이션 · ambient authority · confused deputy
크로스 사이트 요청 위조를 막는 방어 수단
CSRF 토큰 · SameSite · 이중 제출 쿠키 · Sec-Fetch-Site
크로스 사이트 요청 위조가 이용하는 웹 요청 장치
다른 이름: CSRF · Cross-Site Request Forgery · XSRF · Sea Surf · Session Riding · Cross-Site Reference Forgery · Hostile Linking · One-Click attack