Zip Slip in Django with Bearer Tokens
Zip Slip in Django with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Zip Slip is a path traversal vulnerability that occurs when an archive extraction uses user-supplied paths without proper sanitization, allowing files to be written outside the intended directory. In Django, this can manifest when an application accepts an archive file (for example, a configuration or data import) and a token or filename from a Bearer Token is used to derive extraction paths or filenames. If the token or derived path is not validated, an attacker can craft filenames like ../../../etc/passwd inside the archive, causing the server to overwrite sensitive files during extraction.
Bearer Tokens are often passed in the Authorization: Bearer <token> header. In a Django view that processes uploads, a developer might use the token to name extracted files or to authorize the operation without additional path checks. For example, extracting an uploaded archive and using the token directly as a filename prefix can lead to unsafe path joins:
import zipfile
import os
from django.http import HttpRequest
def extract_upload(request: HttpRequest):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if not auth.startswith('Bearer '):
return None
token = auth.split(' ')[1]
# Unsafe: token used directly in path construction
extract_to = os.path.join('/data/uploads', token)
with zipfile.ZipFile(request.FILES['archive'], 'r') as zf:
zf.extractall(extract_to)
return extract_to
If the token contains path sequences (e.g., ../../../secrets) or the archive includes malicious paths, the extraction can escape the target directory. This is especially dangerous when tokens are long and opaque; developers may assume they are safe identifiers, but without validation they remain user-influenced input. The combination of Zip Slip and Bearer Tokens becomes critical when the token-derived path is used in extraction directories or filenames, and when the archive is obtained from an untrusted source.
Django’s file handling utilities do not inherently prevent traversal; it is the developer’s responsibility to sanitize paths. Even when using high-level APIs like UploadedFile, if the token influences the destination path, the attack surface remains. Attackers may also leverage Zip Slip to overwrite application code or configuration files, potentially leading to remote code execution depending on the environment.
Bearer Tokens-Specific Remediation in Django — concrete code fixes
To mitigate Zip Slip when using Bearer Tokens in Django, ensure that token values are never used directly in filesystem paths. Instead, treat tokens as opaque identifiers and map them to safe, application-controlled paths. Below are concrete, safe patterns for handling Bearer Tokens in file extraction scenarios.
1. Validate and sanitize paths
Use os.path.normpath and enforce that resolved paths remain within a base directory. Do not trust tokens or filenames from archives.
import os
import zipfile
from django.http import HttpRequest
def is_within_directory(directory, target):
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)
prefix = os.path.commonprefix([abs_directory, abs_target])
return prefix == abs_directory
def extract_upload_safe(request: HttpRequest):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if not auth.startswith('Bearer '):
return None
token = auth.split(' ')[1]
# Use token as a non-path identifier, not in the filesystem path
base_dir = '/data/uploads'
# Map token to a safe subdirectory (e.g., via database lookup)
user_dir = os.path.join(base_dir, 'user_data')
if not is_within_directory(base_dir, user_dir):
raise PermissionError('Invalid path')
with zipfile.ZipFile(request.FILES['archive'], 'r') as zf:
for member in zf.namelist():
member_path = os.path.normpath(member)
full_path = os.path.join(user_dir, member_path)
if not is_within_directory(base_dir, full_path):
continue # skip malicious entries
zf.extract(member, user_dir)
return user_dir
2. Avoid token-derived filesystem paths entirely
Do not concatenate tokens into paths. Instead, use a fixed directory and reference the token in a database or cache to associate uploads with users.
import uuid
import zipfile
from django.http import HttpRequest
def extract_with_mapping(request: HttpRequest):
auth = request.META.get('HTTP_AUTHORIZATION', '')
if not auth.startswith('Bearer '):
return None
token = auth.split(' ')[1]
# Validate token format (e.g., UUID) if applicable
if not token.replace('-', '').isalnum():
raise ValueError('Invalid token')
# Use a random, safe subdirectory
safe_dir = os.path.join('/data/uploads', str(uuid.uuid4()))
os.makedirs(safe_dir, exist_ok=True)
with zipfile.ZipFile(request.FILES['archive'], 'r') as zf:
for member in zf.namelist():
member_path = os.path.normpath(member)
full_path = os.path.join(safe_dir, member_path)
if not full_path.startswith(safe_dir):
continue
zf.extract(member, safe_dir)
# Store association: token -> safe_dir in your DB/cache
return safe_dir
3. Use Django’s Storage API with caution
If you must store files, configure a custom storage backend that restricts paths and does not allow directory traversal. Always validate filenames before passing them to storage methods.
from django.core.files.storage import FileSystemStorage
import os
class SafeStorage(FileSystemStorage):
def get_valid_name(self, name):
# Remove path components and unsafe characters
name = os.path.basename(name)
return super().get_valid_name(name)
def get_available_name(self, name, max_length=None):
name = self.get_valid_name(name)
return super().get_available_name(name, max_length)
# Usage in a view:
storage = SafeStorage(location='/data/uploads')
for f in request.FILES.getlist('files'):
storage.save(f.name, f)
These patterns ensure that Bearer Tokens influence authorization and identification but never directly dictate filesystem layout, effectively neutralizing Zip Slip risks tied to token-derived paths.