HIGH zip slipdjangomutual tls

Zip Slip in Django with Mutual Tls

Zip Slip in Django with Mutual Tls — 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 often manifests when an application extracts a client-provided ZIP file to a destination derived from archive member names. The presence of Mutual TLS (mTLS) does not prevent Zip Slip; it changes the trust boundary but does not alter how the archive is processed server-side. mTLS ensures that only authenticated clients with valid certificates can reach the endpoint, which may increase the perceived safety of accepting file uploads. However, once the request is authenticated via client certificate, the application must still validate and sanitize paths within the uploaded archive. Without explicit path checks, an attacker with a valid certificate can craft an archive containing entries like ../../../etc/passwd or absolute paths that escape the extraction directory. The combination of mTLS and Zip Slip is notable because mTLS narrows the attack surface to certificate-holding actors, but if authorization and input validation are weak, a compromised or malicious certificate holder can still trigger insecure extraction behavior. This scenario is especially risky when the Django view trusts the client identity provided by mTLS and skips additional checks, assuming the request is safe because it came from an authenticated source. The vulnerability is not introduced by mTLS but is exposed when path normalization is omitted after authentication. Django does not automatically sanitize archive member paths, so developers must explicitly reject paths that traverse parent directories or resolve outside the target folder. In a typical flow, the view receives a file via a POST request, passes it to a library such as Python’s zipfile, and writes entries to disk. If the view uses the member’s filename directly—e.g., extracted_path = os.path.join(UPLOAD_DIR, member.name)—and does not call functions like os.path.normpath combined with prefix checks, absolute or relative paths can escape. mTLS may also influence logging and audit trails, making it easier to trace which certificate triggered the extraction, but it does not mitigate the traversal itself. To safely handle uploads in this setup, Django must enforce strict path validation regardless of transport-layer authentication, ensuring every entry is confined to the intended directory tree.

Mutual Tls-Specific Remediation in Django — concrete code fixes

Remediation for Zip Slip in Django should focus on secure archive extraction and strict path validation, independent of mTLS. However, when mTLS is in use, ensure that client identity is mapped to a least-privilege scope and that extraction logic does not implicitly trust certificate-derived metadata. Below are concrete code examples for secure extraction in a Django view using Mutual TLS for client authentication.

Secure ZIP extraction with path validation

Use a helper that checks each archive entry against a normalized target directory, rejecting paths that escape the intended base. This approach works alongside mTLS authentication.

import os
import zipfile
from django.conf import settings
from django.http import HttpResponseBadRequest

def safe_extract_zip(zip_path, extract_dir):
    """Extract a ZIP file ensuring no path traversal."""
    os.makedirs(extract_dir, exist_ok=True)
    with zipfile.ZipFile(zip_path, 'r') as zf:
        for member in zf.infolist():
            # Normalize the member path
            member_path = os.path.normpath(member.filename)
            # Build the full destination path
            dest_path = os.path.normpath(os.path.join(extract_dir, member_path))
            # Ensure the destination is still inside extract_dir
            if not dest_path.startswith(os.path.abspath(extract_dir) + os.sep):
                raise ValueError(f"Invalid path outside target directory: {member.filename}")
            # Optionally, reject absolute paths or Windows drive letters
            if os.path.isabs(member_path) or (os.sep == '\\' and len(member_path) > 1 and member_path[1] == ':'):
                raise ValueError(f"Absolute path detected: {member.filename}")
            zf.extract(member, dest_path)
    return dest_path

class UploadView(View):
    def post(self, request):
        uploaded_file = request.FILES.get('archive')
        if not uploaded_file:
            return HttpResponseBadRequest('No file uploaded')
        # Save temporarily
        temp_path = f'/tmp/{uploaded_file.name}'
        with open(temp_path, 'wb+') as dst:
            for chunk in uploaded_file.chunks():
                dst.write(chunk)
        try:
            # Extract safely
            safe_extract_zip(temp_path, settings.MEDIA_ROOT)
        except (zipfile.BadZipFile, ValueError) as e:
            return HttpResponseBadRequest(f'Invalid archive: {e}')
        finally:
            if os.path.exists(temp_path):
                os.remove(temp_path)
        return HttpResponse('Extraction successful')

Django settings for client certificate verification

When using Mutual TLS, configure Django to require client certificates and map them to application-level permissions. Below is an example using django-sslserver-style settings and a custom middleware to bind the certificate subject to the request user.

# settings.py
SECURE_SSL_REDIRECT = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

# Assume the web server terminates TLS and sets headers like SSL_CLIENT_VERIFY
# and SSL_CLIENT_S_DN_CN. The middleware reads them.
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'yourapp.middleware.ClientCertMiddleware',
    # ...
]

# yourapp/middleware.py
import ssl
from django.contrib.auth.models import User
from django.http import HttpResponseForbidden

class ClientCertMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # The web server should validate the client certificate and set these headers
        cert_verified = request.META.get('SSL_CLIENT_VERIFY', '')
        if cert_verified != 'SUCCESS':
            return HttpResponseForbidden('Client certificate required')
        subject = request.META.get('SSL_CLIENT_S_DN_CN')
        if subject:
            # Map subject to a local user; in practice, use a lookup table or LDAP
            user, _ = User.objects.get_or_create(username=subject)
            request.user = user
        else:
            request.user = User.objects.get(username='anonymous')
        return self.get_response(request)

Additional hardening tips

  • Always use os.path.normpath and prefix checks before extracting.
  • Consider using pathlib.Path with resolve() and ensuring the result is under the base directory.
  • Limit file permissions on extracted files and avoid executing uploaded content.
  • Log extraction attempts with client certificate identifiers for audit trails.

Frequently Asked Questions

Does Mutual TLS prevent Zip Slip in Django?
No. Mutual TLS authenticates clients but does not sanitize archive paths. You must still validate and restrict file extraction paths to prevent traversal.
How can I safely extract user-uploaded ZIP files in a Django view with mTLS?
Use a dedicated extraction function that normalizes paths with os.path.normpath, checks that each resolved path remains inside the target directory, and rejects absolute or Windows drive paths before extracting.