안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

장고 (Django)

[Django] CSRF 방지 메커니즘 (feat. CsrfViewMiddleware)

피그브라더 2021. 5. 4. 21:41

이 포스팅은 Django가 CSRF를 방지하는 기본적인 메커니즘에 대해 다룬다. 만약 CSRF가 무엇인지 모른다면 이 포스팅을 먼저 읽어보고 오기를 권장한다. 또한 '기본적인' 메커니즘이라고 말한 것에서 알 수 있듯이, 원한다면 CSRF 방지 메커니즘을 어느 정도는 커스터마이징 하는 것도 가능하다. 다만 여기서는 기본 설정에 대해서만 다룰 뿐이다.

 

여기서 설명할 내용을 먼저 한 장의 그림으로 요약하자면 다음과 같다. 이후의 설명은 해당 그림을 토대로 진행하도록 하겠다. 목차를 보는 느낌으로 먼저 한 번 쓰윽 살펴보기 바란다.

 

 

결론부터 얘기하자면, Django의 기본적인 CSRF 방지 메커니즘은 CSRF 토큰을 쿠키(세션이 아니라)에 저장하는 방식을 채택한다. 참고로 Django 공식 문서의 설명을 가져오자면, 이것은 Django에서 권장되는 방식이고 다른 웹 프레임워크들에서는 세션에 CSRF 토큰을 저장하는 방식이 더욱 흔하게 사용된다고 한다.

Storing the CSRF token in a cookie (Django’s default) is safe, but storing it in the session is common practice in other web frameworks and therefore sometimes demanded by security auditors.

 

1. GET 요청을 보낼 때 (= POST 요청을 보낼 수 있는 페이지에 방문)

 

먼저, <form> 태그나 Ajax를 이용하여 POST 요청을 보내는 기능이 존재하는 페이지에 방문하는 상황을 생각해 보자. 예를 들면 게시글 작성 페이지와 같은 경우이다. CSRF 방지 메커니즘의 핵심은 해당 POST 요청을 이러한 페이지에서만 보낼 수 있도록 제한하는 것에 있다. 왜냐하면, 대부분의 CSRF는 올바르지 않은 페이지에서 발생된 POST 요청에 의한 것이기 때문이다. 그렇다면 구체적으로 그러한 제한을 어떻게 하는 건지 한 번 알아보도록 하자.

 

1-1. django.template.context_processor.csrf (→ Context Processor)

① CSRF 토큰을 생성하여 request.META['CSRF_COOKIE']에 저장한다. (만약 이미 저장되어 있다면 생략)

② 해당 CSRF 토큰을 context['csrf_token']에 첨가한다.

 

Django에서 Context란 뷰가 템플릿을 렌더링 할 때 템플릿에게 데이터를 전달하기 위해 사용하는 매개 객체이다. 그리고 Context Processor란 그러한 Context에 특정 데이터를 첨가해주는 역할을 수행한다. 이로 인해 템플릿에서는 해당 데이터를 템플릿 변수의 형태로 참조할 수 있게 된다. 대표적인 Context Processor로는 django.template.context_processors.request가 있다. 이는 Context에 request 객체를 첨가해줌으로써 템플릿에서 request라는 이름의 템플릿 변수로 해당 객체를 참조할 수 있게 한다. 참고로 이 Context Processor는 Django에서 기본적으로 활성화되어 있다(설정 파일의 TEMPLATES 변수 참고).

 

①에서 CSRF 토큰을 request.META['CSRF_COOKIE']에 저장하는 이유는 나중에 CsrfViewMiddleware가 해당 CSRF 토큰을 request 객체로부터 읽어 이를 쿠키에 설정하기 위함이다. 그리고 그렇게 쿠키가 설정된 이후부터는 매 요청마다 쿠키로 전송되는 CSRF 토큰이 CsrfViewMiddleware에 의해 request.META['CSRF_COOKIE']에 저장된다. 이때부터는 ①의 과정이 생략된다.

 

그리고 ②에서 CSRF 토큰을 Context에 첨가한 것은 템플릿에서 {% csrf_token %} 템플릿 태그를 사용할 수 있도록 하기 위함이다.

 

그런데 여기서 하나 짚고 넘어가야 할 부분이 있다. django.template.context_processors.request와 같이 기본적으로 활성화되어 있는 Context Processor와는 달리, 이 Context Processor는 설정 파일의 TEMPLATES 변수를 보더라도 기본적으로 활성화되어 있지 않다. 그렇다면 명시적으로 TEMPLATES 변수를 수정하여 이를 활성화시켜야 하는 것일까? 그렇지 않다. RequestContext 클래스의 context 객체를 사용하는 경우라면 이 Context Processor는 자동으로(묵시적으로) 항상 활성화된다. 그리고 render() 함수나 제네릭 뷰 등을 사용하는 경우에는 기본적으로 RequestContext 클래스의 context 객체가 사용된다. 따라서 일반적인 경우에는 명시적으로 이를 활성화시켜줄 필요가 없다.

 

1-2. {% csrf token %} (→ Template Tag)

context['csrf_token'] 값을 읽어서 hidden <input> 태그를 렌더링 한다. 이때 렌더링 되는 <input> 태그의 name 어트리뷰트 값은 'csrfmiddlewaretoken'이다. 이름이 이러한 이유는 이 <input> 태그에 의해 전송되는 값을 CsrfViewMiddleware가 읽어서 검증하기 때문이다(뒤에서 더 자세히 설명할 예정). 그리고 당연한 얘기겠지만, 이 템플릿 태그를 사용하려면 context['csrf_token'] 값이 존재해야 하므로 앞서 설명했던 csrf라는 Context Processor가 반드시 활성화되어 있어야 한다.

 

1-3. CsrfViewMiddleware (→ Middleware)

뷰가 템플릿 렌더링까지 마무리하여 만들어 낸 response 객체를 후처리 하는 부분이다. request.META['CSRF_COOKIE'] 값을 읽어 이를 쿠키에 'csrftoken'이라는 이름으로 설정한다. 이 이후부터는 매 요청마다 쿠키로 해당 CSRF 토큰이 함께 전송될 것이다.

 

2. POST 요청을 보낼 때 (= POST 요청의 유효성을 검증)

 

그러면 이제 POST 요청에 대한 유효성 검증을 어떻게 하는지 알아보자. 우선, CSRF 토큰이 'csrftoken'이라는 이름의 쿠키로 잘 전송이 되었는지 확인한다. 만약 해당 쿠키가 없다면 유효하지 않은 POST 요청으로 판단한다. 그런데 해당 쿠키가 존재한다면 그 쿠키의 값을 읽어서 다음 둘 중 하나의 값과 같은지 검사한다. 하나는 'csrfmiddlewaretoken'이라는 이름의 POST 파라미터, 다른 하나는 'X_CSRFToken'이라는 이름의 요청 헤더이다. 이 둘 중 어느 것의 값과도 일치하지 않는다면 유효하지 않은 POST 요청으로 판단하고, 하나의 값과 일치한다면 유효한 POST 요청을 판단한다. 이러한 모든 검증 과정은 CsrtViewMiddleware가 request 객체를 전처리 할 때 이뤄지며, 유효한 POST 요청으로 판단되면 적절한 뷰를 호출하지만 그렇지 않으면 403 응답을 반환하도록 한다.

 

결국 <form> 태그를 이용하여 POST 요청을 보내는 경우라면, {% csrf_token %} 템플릿 태그를 사용하여 'csrfmiddlewaretoken'이라는 이름의 hidden <input> 태그를 렌더링 함으로써 유효한 POST 요청으로 판단될 수 있다.

 

또한 Ajax를 이용하여 POST 요청을 보내는 경우라면, 쿠키에 설정되어 있는 CSRF 토큰을 읽어서 요청의 'X_CSRFToken' 헤더에 설정함으로써 유효한 POST 요청으로 판단될 수 있다(Django 공식 문서 참조).

 

참고로, CsrfViewMiddleware는 POST 요청의 유효성을 검증하기 전에 먼저 쿠키로 전송된 CSRF 토큰을 request.META['CSRF_COOKIE']에 저장하는 일을 수행한다(쿠키가 전송되지 않았다면 이 과정은 생략). 이는 계속 동일한 CSRF 토큰을 사용하기 위한 로직이다. 이렇게 하지 않으면 다음에 또 다른 페이지로의 GET 요청을 보낼 때 새로운 CSRF 토큰을 생성하여 request.META['CSRF_COOKIE']에 저장하게 될 것이기 때문이다.

 

3. @ensure_csrf_cookie 데코레이터

뷰에 설정할 수 있는 일종의 데코레이터로, 항상 CSRF 토큰이 쿠키로 설정될 수 있도록 하는 역할을 수행한다. 그림에 표시된 ⓐ 과정과 사실상 동일하다. 즉, CSRF 토큰을 생성하여 request.META['CSRF_COOKIE']에 저장하는 것이다.

 

그런데 앞서 말했듯 대부분의 경우에는 이 데코레이터를 사용하지 않아도 CSRF 토큰이 쿠키로 설정이 될 것이다. 보통 render() 함수나 제네릭 뷰를 이용하여 뷰를 구현하기에 RequestContext 타입의 context 객체가 사용되는데, 그러면 자동으로 csrf라는 이름의 Context Processor가 활성화되어 쿠키로 설정할 CSRF 쿠키가 생성되어 request.META['CSRF_COOKIE']에 저장되기 때문이다.

 

4. 결론 : CSRF를 방지하는 핵심 원리

지금까지 설명한 것은 CSRF 방지 메커니즘 그 자체였다. 즉, 이 메커니즘이 왜 CSRF를 방지하는지는 설명하지 않았다. 물론 CSRF의 개념에 익숙한 사람은 스스로 눈치를 챘을 수도 있겠지만, 포스팅의 완전성을 위해 여기서 간단히라도 설명을 하려 한다.

 

이 포스팅에서 설명한 CSRF 방지 메커니즘의 핵심은 바로 'CSRF 토큰을 쿠키에 저장한다는 것'이다. 여기서의 키워드는 '쿠키'인데, 브라우저의 기본적인 보안 정책을 활용한 방어 기법이기 때문이다. 브라우저의 기본적인 보안 정책에 따르면, JavaScript를 이용하여 다른 도메인의 쿠키를 설정하는 것은 불가능하게 되어 있다. 따라서 임의의 난수로 생성한 CSRF 토큰을 쿠키로 전송함과 동시에 POST 파라미터 혹은 요청 헤더에 실어 보내고 서버에서는 그 둘이 같은지를 검사함으로써 유효성을 검증할 수 있게 된다. 해커 입장에서 봤을 때 CSRF 토큰을 생성하여 POST 파라미터나 요청 헤더에 실어 보내는 것은 어려운 일이 아니겠지만 쿠키로는 전송할 수 없기 때문이다.

 

물론 한계점도 존재하는 방어 기법이다. 동일한 도메인 내의 페이지에 XSS 취약점이 발생한 경우에 문제가 된다. 동일한 도메인 내에서는 CSRF 토큰이 쿠키로 설정될 수 있기 때문에 그것을 POST 파라미터나 요청 헤더에 실어 보내면 유효한 POST 요청으로 판단될 것이기 때문이다. 이에 대한 해결책으로는 여러 가지가 존재하겠지만, 여기서는 다루지 않도록 하겠다.