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)