Zip Slip in Django
How Zip Slip Manifests in Django
Zip Slip vulnerabilities in Django applications typically occur when user-supplied archives are extracted without proper path validation. Django's file handling utilities, while robust for many scenarios, don't automatically prevent path traversal in extracted files. The vulnerability allows attackers to write files outside the intended directory by including '../' sequences in filenames within the archive.
A common Django pattern that's vulnerable looks like this:
import zipfile
from django.core.files.storage import default_storage
from django.conf import settings
def upload_view(request):
if request.method == 'POST':
archive = request.FILES['archive']
path = settings.MEDIA_ROOT
with zipfile.ZipFile(archive) as zip_ref:
zip_ref.extractall(path) # Vulnerable: no path validation
return HttpResponse('Upload successful')
The critical issue is that zipfile.extractall() doesn't validate paths. An attacker can create a ZIP file containing:
../../settings.py
../../manage.py
etc/passwd
When extracted, these files overwrite critical application files or sensitive system files. In Django specifically, this could allow overwriting settings.py to inject malicious configurations, replace manage.py to execute arbitrary code, or write to locations that will be served by django.contrib.staticfiles.
Another Django-specific scenario involves media file uploads where extracted files are later served by django.views.static.serve or through MEDIA_URL. An attacker could place a malicious .py file in a location that Django will attempt to import, leading to remote code execution.
The vulnerability is particularly dangerous in Django because of its dynamic nature—Python files can contain arbitrary code that will execute when imported, and Django's auto-reloading development server will automatically reload modified files, potentially executing malicious code immediately.
Django-Specific Detection
Detecting Zip Slip vulnerabilities in Django requires examining both code patterns and runtime behavior. Code review should focus on these specific Django patterns:
import re
def is_zip_slip_vulnerable(code):
patterns = [
r'zipfile\.ZipFile\(.*\).extractall\(',
r'extractall\(',
r'ZipFile\(.*\)\.extract\(',
r'extract\(',
r'pathlib\.Path\(\).*\.mkdir\(parents=True\)',
r'os\.makedirs\(.*parents=True\)'
]
for pattern in patterns:
if re.search(pattern, code):
return True
return False
Static analysis tools like Bandit have specific Django checks, but they often miss context-specific vulnerabilities. For comprehensive detection, middleBrick's API security scanner examines Django applications for Zip Slip vulnerabilities by:
- Analyzing file extraction endpoints for path traversal patterns
- Testing with crafted ZIP archives containing
../sequences - Verifying that extracted files don't escape the intended directory
- Checking if extracted files are later served or executed by Django
middleBrick's scanner specifically tests Django applications by submitting ZIP files with paths like ../../django_app/settings.py and ../../templates/malicious.html, then verifies if these files appear outside the intended extraction directory. The scanner also checks for Django-specific patterns like extracting to STATIC_ROOT or MEDIA_ROOT where files might be served without proper validation.
Runtime detection involves monitoring file operations and logging suspicious path patterns. Django middleware can log extraction attempts and flag when paths contain directory traversal sequences:
import logging
from django.utils.deprecation import MiddlewareMixin
logger = logging.getLogger(__name__)
class ZipSlipDetectionMiddleware(MiddlewareMixin):
def process_request(self, request):
if 'extract' in request.path and request.method == 'POST':
if '../' in request.body.decode():
logger.warning('Potential Zip Slip attempt detected')
return HttpResponseForbidden('Invalid file path')
return None
Django-Specific Remediation
The most effective remediation for Zip Slip in Django is to validate and sanitize all paths before extraction. Django's pathlib module provides robust path manipulation that can prevent traversal:
from pathlib import Path
import zipfile
from django.core.files.storage import default_storage
from django.conf import settings
def safe_extract(zip_path, extract_dir):
extract_dir = Path(extract_dir).resolve()
with zipfile.ZipFile(zip_path) as zip_ref:
for member in zip_ref.namelist():
# Check for path traversal
if '..' in member or member.startswith('/'):
raise ValueError(f'Illegal path in ZIP archive: {member}')
# Get full path and ensure it's within the extract directory
member_path = (extract_dir / member).resolve()
if not member_path.is_relative_to(extract_dir):
raise ValueError('ZIP archive path traversal attempt detected')
# Create parent directories if needed
member_path.parent.mkdir(parents=True, exist_ok=True)
# Extract file
with zip_ref.open(member) as source, open(member_path, 'wb') as target:
target.write(source.read())
def upload_view(request):
if request.method == 'POST':
archive = request.FILES['archive']
temp_path = default_storage.save('temp_upload.zip', archive)
try:
safe_extract(temp_path, settings.MEDIA_ROOT)
default_storage.delete(temp_path)
except ValueError as e:
default_storage.delete(temp_path)
return HttpResponseBadRequest(str(e))
return HttpResponse('Upload successful')
This approach validates each file path before extraction, ensuring it cannot escape the intended directory. The is_relative_to() method provides robust protection against path traversal.
For Django applications using django-storages or cloud storage backends, the validation logic remains the same, but extraction targets different storage systems:
from storages.backends.s3boto3 import S3Boto3Storage
import boto3
def safe_extract_to_s3(zip_path, bucket_name, prefix=''):
s3 = boto3.client('s3')
with zipfile.ZipFile(zip_path) as zip_ref:
for member in zip_ref.namelist():
if '..' in member or member.startswith('/'):
raise ValueError(f'Illegal path in ZIP archive: {member}')
# Ensure prefix doesn't allow traversal
if '../' in prefix:
raise ValueError('Invalid prefix for S3 extraction')
# Upload to S3
key = f'{prefix.rstrip('/')}/{member.lstrip('/')}'
with zip_ref.open(member) as file_obj:
s3.upload_fileobj(file_obj, bucket_name, key)
Django middleware can also provide an additional layer of protection by intercepting file uploads and scanning archives before processing:
import zipfile
from io import BytesIO
class ZipSlipPreventionMiddleware(MiddlewareMixin):
def process_request(self, request):
if 'archive' in request.FILES:
archive = request.FILES['archive']
try:
with zipfile.ZipFile(archive) as zip_ref:
for member in zip_ref.namelist():
if '..' in member or member.startswith('/'):
return HttpResponseForbidden('Invalid file path in archive')
except zipfile.BadZipFile:
return HttpResponseBadRequest('Invalid ZIP file')
return None