Zip Slip in Django with Basic Auth
Zip Slip in Django with Basic Auth — 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. In Django, this risk is compounded when Basic Authentication is used to protect an endpoint that accepts file uploads or archive downloads. Basic Auth secures the HTTP layer, but it does not constrain how the server handles file paths inside extracted archives. If a view extracts a zip file using the provided filename or a value derived from user input, an attacker can include paths like ../../../etc/passwd or absolute paths that escape the intended directory. The authentication gate may prevent unauthenticated access to the endpoint, but once credentials are supplied, the extraction logic runs with the server process permissions, enabling unauthorized file reads or writes.
Consider a Django view that allows users to download and extract a zip archive. If the archive contains malicious entries crafted with traversal sequences, and the code uses zipfile.extractall() without validating member paths, the server can be forced to write outside the target directory. In this scenario, Basic Auth gives a false sense of security: the endpoint is not publicly exposed, but the underlying extraction is unsafe. Attackers who obtain valid credentials (e.g., via phishing or credential stuffing) can exploit the Zip Slip path traversal to access or overwrite sensitive files. This is especially dangerous when the same endpoint exposes administrative functionality or interacts with the local filesystem for reporting or data export.
middleBrick detects this risk pattern during unauthenticated black-box scanning and during active probe sequences that include authentication when configured. Even though the scan may initially test the endpoint without credentials, findings related to unsafe extraction and path handling are surfaced with severity and remediation guidance. The scanner cross-references the OpenAPI spec to identify endpoints that accept file parameters and checks whether server-side validation is evident in the contract. For endpoints protected by Basic Auth, the scan notes that authentication reduces the attack surface but does not eliminate the traversal risk, emphasizing the need for secure extraction logic regardless of transport layer protections.
Basic Auth-Specific Remediation in Django — concrete code fixes
To remediate Zip Slip in Django when using Basic Authentication, ensure that any archive extraction normalizes and restricts paths to a safe base directory. Do not trust filenames from the archive or from user input. Use Python’s os.path.normpath and explicit prefix checks, or use libraries that handle this safely. Below are concrete, working examples that combine Django views with HTTP Basic Authentication and secure extraction logic.
Secure Django view with Basic Auth and safe zip extraction
import os
import zipfile
from django.http import JsonResponse
from django.views.decorators.http import require_httpMethods
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from base64 import b64decode
@csrf_exempt
@require_http_methods(["POST"])
def extract_archive(request):
# Example Basic Auth header: Basic base64(username:password)
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
if not auth_header.startswith("Basic "):
return JsonResponse({"error": "Unauthorized"}, status=401)
try:
token = auth_header.split(" ")[1]
decoded = b64decode(token).decode("utf-8")
username, password = decoded.split(":", 1)
# Replace with your actual authentication check
if not (username == "admin" and password == "secret"):
return JsonResponse({"error": "Invalid credentials"}, status=401)
except Exception:
return JsonResponse({"error": "Malformed authorization header"}, status=401)
archive_file = request.FILES.get("archive")
if not archive_file:
return JsonResponse({"error": "No archive provided"}, status=400)
base_dir = os.path.abspath("/safe/extraction/directory")
with zipfile.ZipFile(archive_file, "r") as zf:
for member in zf.namelist():
# Normalize path and ensure it stays within base_dir
member_path = os.path.normpath(member)
dest_path = os.path.normpath(os.path.join(base_dir, member_path))
if not dest_path.startswith(base_dir + os.sep):
return JsonResponse({"error": f"Invalid path detected: {member}"}, status=400)
zf.extract(member, dest_path)
return JsonResponse({"message": "Extraction completed safely"})
Key points in the example:
- Basic Auth credentials are manually parsed and validated against a simple check; in production, integrate with Django’s authentication backend (e.g.,
authenticate()). os.path.normpathremoves redundant separators and up-level references, but is not sufficient alone — you must also verify that the resolved path starts with the intended base directory.- The check
dest_path.startswith(base_dir + os.sep)prevents directory traversal even if the archive contains crafted paths or absolute paths on some platforms.
Alternative: Using zipfile.extract with explicit member paths
import os
import zipfile
def safe_extract_zip(zip_path, extract_to):
with zipfile.ZipFile(zip_path, "r") as zf:
for member in zf.infolist():
member_path = os.path.normpath(member.filename)
dest = os.path.normpath(os.path.join(extract_to, member_path))
if os.path.commonpath([dest, extract_to]) != extract_to:
raise ValueError(f"Invalid member path: {member.filename}")
zf.extract(member, dest)
This pattern is useful when you want to encapsulate extraction logic and raise explicit errors for unsafe members. Always validate on the server regardless of client-side checks, and avoid using extractall without path validation.