Potential XSS bug found in Django Rest Framework

Potential XSS bug found in Django Rest Framework
Ueno, Tokyo, Japan

안녕하세요 웹 해킹을 하고 있는 윤석찬입니다. 오랜만에 인사드립니다. 이번에는 한국 분들에게도 제 블로그를 노출시키고자, 한국어로 글을 쓰게 되었습니다.

최근 Django Rest Framework 소스코드를 살펴보다가, 부주의하게 사용했을 경우 XSS 공격으로부터 취약할 수 있는 기능을 발견해서 블로그에 정리해봅니다. 이미 Django Security 팀에는 report를 되어 인지된 이슈이기 때문에 블로그에 게시해도 되겠다 싶었고, 무엇보다 찾은 이슈가 저만 알기에는 아쉬웠네요 ㅎㅎ;

여러분들은 아래 코드에서 XSS가 발생할 수 있다고 생각하시나요? 발생한다면 왜 발생한다고 생각하시나요?

views.py

참고로 위 지정된 경로로 접속해보면 아래처럼 REST API의 예시 화면을 보실 수 있습니다.

Django Rest Framework 기본 API Documentation 화면

아마도 한번에 맞추신 분들은 거의 없을 것 같습니다. rest_framework.response.Response 클래스의 객체에 직접 헤더를 지정해 응답할 때, XSS 공격에 취약할 수 있다는 사실이 어떤 문서에서도 명시되지 않았기 때문입니다.

그럼 이 글에서는 rest_framework.response.Response 클래스를 통해 헤더를 직접 지정하는 것이 왜 위험한지 알아보도록 하겠습니다.

Django Rest Framework

Django Rest Framework(DRF)는 Django로 제작된 웹 서버에서 쉽게 HTTP기반 REST API를 구현할 수 있도록 도와주는 3rd party 라이브러리입니다. 요새는 Django Ninja라는 라이브러리가 크게 인기를 얻고 있지만, 당장 몇 년 전까지만해도 Django 기반의 REST API를 구현하기 위해서는 DRF 라이브러리 이외에는 쓸만한 라이브러리가 없었습니다. 제가 글을 쓰고 있는 현 시점 깃허브 스타 27.6k를 받았을 만큼 범용적인 라이브러리라고 볼 수 있겠네요.

GitHub - encode/django-rest-framework: Web APIs for Django. 🎸
Web APIs for Django. 🎸. Contribute to encode/django-rest-framework development by creating an account on GitHub.

Django Security Team으로부터 보안 취약점을 받고 있으니 사실상 공식 라이브러리라고 보아도 무방해보입니다.

from Django REST Framework Webpage
Home - Django REST framework
Django, API, REST, Home

How Django Template Prevents XSS ?

DRF에서 발생할 수 있는 XSS 취약점을 이해하기 위해서는、Django의 template 기능을 제대로 알아볼 필요가 있습니다. Django는 자체 template 기능을 통해 XSS로부터의 위협을 쉽게 제거했다는 점으로 유명합니다.

views.py

위는 Django의 render() 메소드를 사용해서, 지정된 template에 사용자의 입력값을 출력하는 예시코드입니다.

Example Template

test.html 파일은 view 메소드로부터 name 값을 받아 출력도록 작성되어 있습니다.

Response Text

실제로 이 엔드포인트에 요청을 보내보면 HTML Entity 인코딩을 통해 특수문자를 처리해둔 것을 볼 수 있습니다. 그렇다면 Django Template 내부에서 특수문자를 인코딩 없이 그대로 사용하려면 어떻게 해야할까요? 이때 사용할 수 있는 것이 safe template filter 입니다.

safe template filter

Django의 Template Filter는 위 코드처럼 사용할 수 있습니다.

The result ofsafe template filter

이번에는 특수문자가 인코딩되지 않고 그대로 출력되었습니다.

Analyzing Django safe Template Filter

그렇다면 이번에는 safe template filter의 작동 방식을 간단하게 분석해보겠습니다.

The source code of safe template filter

safe Template Filter에서는 인자를 받아 내부적으로 mark_safe() 메소드를 실행 한 뒤 리턴합니다. make_safe() 메소드에서는 SafeString 이라는 클래스를 통해서, 일반적인 str type의 데이터를 명시적으로 SafeString 클래스 오브젝트로 변환하고 있습니다.

글이 길어질 것 같아서 더 자세히는 설명하지 않겠지만, 최종적으로 Django가 지정된 Template을 기반으로 HTTP Response Text를 만들 때 문자열의 타입이 SafeString이라면 그대로 출력하고, SafeString 이외의 타입이라면 특수문자를 인코딩합니다.

그렇다면

  1. safe Template Filter를 사용하거나
  2. mark_safe() 메소드를 사용해서

사용자의 입력값이 포함된 데이터를 처리하게 되면, 잠재적으로 XSS 공격에 취약할 수 있겠네요. 🤔

Django Rest Framework 소스코드 분석

이번에는 Django Rest Framework 라이브러리의 소스코드를 분석해서, APIView 클래스가 어떻게 HTTP Response를 생성하는지 살펴보겠습니다.

rest_framework/settings.py

rest_framework/settings.py

약간의 소스코드 분석을 통해 rest_framework.renderers.BrowsableAPIRenderer 라는 클래스가 DRF APIView 클래스의 기본 렌더러 클래스임을 알 수 있었습니다. 그럼 이어서 BrowsableAPIRenderer 클래스를 분석해보겠습니다.

rest_framework/renderers.py

rest_framework/renderers.py

이 클래스에서 이르러서야 우리는 DRF가 rest_framework/api.html 파일을 기반으로 Template을 생성하고, API Documentation 화면을 보여준다는 사실을 알게 되었습니다.

rest_framework/api.html

api.html 파일은 다시 같은 디렉토리 내의 base.html 파일을 참조합니다.

rest_framework/base.html

rest_framework/base.html

이제 거의 끝났습니다!

rest_framework/base.html 파일에서는  response_headers 변수를 break_long_headers 라는 Template Filter를 통해 출력해주고 있습니다. 마지막으로 이 template filter를 분석해보겠습니다.

break_long_headers Template Filter

break_long_headers Template Filter

이 template filter에서는 앞서 base.html 파일에서 보았듯이, Response Header의 Key, Value 중 Value에 해당되는 값이 header라는 인자로 들어옵니다. 그런데 이 함수에 mark_safe() 메소드가 등장합니다!

mark_safe() 메소드는 이전에 설명드린대로, 인자를 안전하다고 명시적으로 표시하고 HTML Entity 인코딩을 수행하지 못하도록 합니다. 따라서 header 인자에 사용자의 입력값이 포함되는 경우, 특수문자를 인코딩하지 않고 그대로 출력해서 XSS 공격에 취약합니다.

Exploitation 🚀

views.py

그럼 다시 처음으로 돌아가 views.py 소스코드를 분석해보겠습니다.

우리는 방금 일련의 분석 과정을 통해 APIView 클래스에서 반환되는 Response Header 값에 사용자의 입력값이 포함되면 XSS 공격에 취약하다는 사실을 알게 되었습니다. 그런데 이 소스코드.. Set-Cookie 헤더를 세팅하는데 사용자로부터 받은 값을 포함하고 있네요. 그렇다면…

http://localhost:8000/api/?foo=<img+src=x+onerror=prompt()>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,

XSS 성공입니다!

그러나..

그러나 이 XSS는 반쪽짜리 취약점입니다.

첫번째로 Response Header에 사용자의 입력값이 포함되는 경우는 극히 드뭅니다. 사용자 입력이 포함되는 경우는 Set-Cookie, Location, Content-Location 정도가 있겠네요.

두번째로 만약 Response Header에 사용자의 입력값이 들어간다고 해도, RFC 3986에 따라 그 값은 반드시 URL 인코딩되어야 합니다. 더불어 Set-Cookie 헤더의 경우 원래는 Response 클래스 내부의 set_cookie() 메소드를 통해 지정되는데, 이 메소드는 URL 인코딩하는 코드가 포함되어 있습니다.

따라서 표준 보안 규격을 잘 지켜 개발되었다면 절대로 발견되지 않을 취약점이겠네요. Django Security Team에서도 이 점들을 이유로 이번 이슈에 대해서는 보안 취약점으로서 취급하지 않겠다는 답장을 받았습니다.

결론

  • RFC 규약을 따라 시큐어 코딩 하자
  • 기본 기능 이용하자

추가

저라도 이 버그를 수정하고자 하는 마음에, django-rest-framework의 메인 브랜치에 Pull Request를 보냈고, 검토 끝에 Merge 되었습니다. 3.15.2 버전부터 제 풀리퀘스트가 반영되었네요.

Fix potential XSS vulnerability in break_long_headers template filter by ch4n3-yoon · Pull Request #9435 · encode/django-rest-framework
DescriptionThe header input is now properly escaped before splitting and joining with <br> tags. This prevents potential XSS attacks if the header contains unsanitized user input.This pull reques...

MITRE로부터 이 버그에 대해 CVE-2024-21520도 받았습니다!

NVD - CVE-2024-21520

궁금한 점이 있으시다면 ch4n3.yoon@gmail.com으로 연락주시길 바랍니다.