HIGH symlink attackdjangobasic auth

Symlink Attack in Django with Basic Auth

Symlink Attack in Django with Basic Auth — how this specific combination creates or exposes the vulnerability

A symlink attack in Django with Basic Auth occurs when an authenticated endpoint exposes a file-serving or file-processing interface that resolves user-controlled paths or filenames into filesystem symlinks. If Basic Auth protects the view but the view later uses user input to open, read, or serve files (for example via open(path), FileResponse, or static.serve), an attacker can supply a crafted filename or URL parameter that traverses directories and points to a sensitive file via a symbolic link.

Consider a view that serves uploaded documents after a successful Basic Auth challenge. If the view concatenates a user-supplied filename with a base directory without canonicalizing or validating the resolved path, an attacker can upload or request a file such as ../../../etc/passwd or, more dangerously, a symlink they previously placed in a writable location that points to a protected system file. When the view opens the file, the symlink redirects access, bypassing intended access controls and potentially exposing credentials, application secrets, or other sensitive data.

Basic Auth adds authentication but does not mitigate path traversal or symlink resolution. Because the authentication layer only confirms identity, the application must still enforce strict path validation and filesystem isolation. If the developer assumes authentication equals safe file access, the combination creates a false sense of security. The vulnerability is not in Basic Auth itself but in how the authenticated file-serving logic handles user-controlled paths and symlinks.

In a scenario where Django serves user-uploaded content via a view decorated with login_required or protected by HTTP Basic Auth, an attacker who has valid credentials (or steals a Basic Auth header) can probe directory structures and place symlinks in directories the app can write to. For example, an import feature that extracts archives may extract a symlink into a temporary directory; if the application later reads that symlink with elevated privileges, it can redirect reads or writes to arbitrary files.

Real-world analogs include CVE-class patterns where file disclosure or SSRF chains leverage symlinks in web frameworks. Although not a specific published CVE in Django alone, this class of issue maps to OWASP API Top 10 #1 (Broken Object Level Authorization) and #5 (Broken Function Level Authorization) when file endpoints expose IDs or paths without canonical resolution. The risk is high when combined with endpoints that return file contents directly, because an authenticated but malicious or compromised client can leverage symlinks to reach sensitive locations outside the intended document root.

Basic Auth-Specific Remediation in Django — concrete code fixes

Remediation focuses on removing symlink leverage and enforcing strict path controls in authenticated file operations. Do not rely on Basic Auth or any authentication layer to prevent path manipulation. Instead, canonicalize paths, use allowlists, and avoid direct filesystem access based on user input.

1. Use os.path.realpath and validate within a confined directory

Always resolve user input to an absolute path and ensure it remains inside an allowed base directory. Do not trust relative segments or URL-encoded traversal patterns.

import os
from django.http import FileResponse, HttpResponseForbidden
from django.conf import settings

def serve_file(request, filename):
    # Basic Auth or other auth decorators can be applied above this point
    base_dir = settings.MEDIA_ROOT  # or a dedicated upload directory
    safe_path = os.path.realpath(os.path.join(base_dir, filename))
    if not safe_path.startswith(os.path.realpath(base_dir)):
        return HttpResponseForbidden('Invalid path.')
    try:
        return FileResponse(open(safe_path, 'rb'))
    except FileNotFoundError:
        return HttpResponseForbidden('File not found.')

2. Avoid filesystem APIs that follow symlinks for sensitive operations

If you must work with user input, prefer APIs that do not follow symlinks, or explicitly check for symlinks and reject them.

import os

def is_safe_path(path, base):
    base_real = os.path.realpath(base)
    path_real = os.path.realpath(path)
    if os.path.islink(path):
        return False  # reject explicit symlinks
    return path_real.startswith(base_real)

3. Use Django’s Storage API and avoid raw filesystem access

The Storage API adds a layer of abstraction. Configure a custom storage backend with location constraints and avoid passing user-controlled paths directly to storage methods.

from django.core.files.storage import FileSystemStorage
import os

class SafeStorage(FileSystemStorage):
    def get_valid_name(self, name):
        # Normalize and restrict path components
        return os.path.basename(name)

    def path(self, name):
        # Optionally raise if resolved path escapes intended location
        resolved = super().path(name)
        if not resolved.startswith(self.location):
            raise ValueError('Path traversal detected.')
        return resolved

storage = SafeStorage()
# Use storage.save, storage.open, etc., avoiding raw filesystem calls

4. Prefer serving files via Django views with strict ID mapping

Instead of allowing direct filename parameters, map logical identifiers to filesystem names and serve through a view that enforces ownership and path constraints.

from django.shortcuts import get_object_or_404
from django.http import FileResponse
from .models import Document

def document_detail(request, doc_id):
    doc = get_object_or_404(Document, pk=doc_id)
    # doc.file_path is set at upload time and stored as a safe filename
    return FileResponse(open(doc.file_path, 'rb'), as_attachment=True)

5. Secure file upload handling

Validate file types, rename files on upload, and store them outside the web root or behind a view that enforces access control. Do not preserve original filenames or directory structures from user uploads.

import uuid
import os
from django.core.files.storage import FileSystemStorage

def handle_upload(file_obj):
    ext = os.path.splitext(file_obj.name)[1]
    safe_name = f'{uuid.uuid4().hex}{ext}'
    storage = FileSystemStorage(location='/var/app/uploads')
    return storage.save(safe_name, file_obj)

Frequently Asked Questions

Does using Basic Auth prevent symlink attacks in Django?
No. Basic Auth provides identity verification but does not prevent path traversal or symlink resolution. You must validate and sanitize file paths independently of authentication.
What is the most important mitigation for symlink attacks in authenticated file-serving views?
Canonicalize paths with os.path.realpath, confine files to a strict base directory, reject inputs that resolve outside that directory, and avoid serving files directly based on user-controlled names or symlinks.