Path Traversal in Django with Hmac Signatures
Path Traversal in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Path Traversal occurs when user-controlled input is used to construct file system paths without proper validation, allowing an attacker to access files outside the intended directory. In Django, this typically manifests through file-serving views that concatenate user input (e.g., a filename or key) with a base directory. When Hmac Signatures are used to protect these identifiers, developers may assume the signature itself prevents path traversal. However, if the signature is only validated after the path is constructed, or if the signature covers a subset of the path components, an attacker can still leverage directory sequences like ../ to escape the intended directory.
Consider a Django view that serves documents using a signed, predictable identifier. The view might generate a URL-safe token that includes a filename and an Hmac signature, then use the filename directly in open() after verifying the signature. If the signature is computed over the filename alone and does not canonicalize or reject path traversal sequences, an attacker can submit a filename such as ../../../etc/passwd and a valid signature for ../../../etc/passwd. Because the signature matches, the view trusts the identifier and opens the sensitive file. This pattern is common in download endpoints, document management systems, and any service that uses signed tokens to reference files without enforcing strict allowlists.
Another scenario involves using signed identifiers to reference user-uploaded files stored in a predictable directory. If the view does not normalize paths (for example, by using os.path.normpath or pathlib.Path.resolve()) before joining with the base directory, an attacker can supply a filename like malicious/../secrets.txt. Even when the Hmac signature covers the entire identifier, if the signature is verified on the raw string before path resolution, the traversal component remains part of the resolved path. This can lead to unauthorized read access to files outside the upload directory, potentially exposing configuration files, source code, or other sensitive resources.
The risk is compounded when the signature scheme does not enforce strict input constraints. For instance, if the signed payload includes the filename without validating that it contains only safe characters or that it resolves within an allowed prefix, the signature becomes a trust anchor rather than a security boundary. In such cases, the signature does not mitigate Path Traversal; it may even give a false sense of security. MiddleBrick scans detect these patterns by correlating signature usage with file operation endpoints and identifying whether path canonicalization occurs before signature verification.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To securely use Hmac Signatures in Django and prevent Path Traversal, you must ensure that file paths are strictly controlled before the signature is generated or verified. The safest approach is to avoid using user-supplied filenames directly in paths. Instead, map signed identifiers to a strict allowlist of files or use opaque identifiers that reference an internal index. When you must use filenames, canonicalize and validate them before constructing any filesystem path and ensure the signature covers the canonical, safe representation.
Below are concrete, secure patterns for Django views using Hmac Signatures. The first example uses a mapping from a signed integer ID to a filename stored in a database, ensuring that the filesystem path is never derived directly from user input.
import os
from django.conf import settings
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.http import Http404, HttpResponse
from django.views import View
signer = TimestampSigner()
def get_file_record(file_id: int):
# In practice, retrieve from a model; this is illustrative
files = {
1: 'report.pdf',
2: 'invoice.pdf',
}
return files.get(file_id)
class SecureDownloadView(View):
def get(self, request, signed_token):
try:
# Verify and unwrap the signed token; it contains an integer ID
file_id_str = signer.unsign(signed_token, max_age=3600)
file_id = int(file_id_str)
except (BadSignature, SignatureExpired, ValueError):
raise Http404('Invalid or expired token')
filename = get_file_record(file_id)
if not filename:
raise Http404('File not found')
# Build path from a controlled base directory and a known-safe filename
base = settings.MEDIA_ROOT
path = os.path.join(base, filename)
# Ensure the resolved path remains within the base directory
if not os.path.commonpath([os.path.realpath(path), os.path.realpath(base)]) == os.path.realpath(base):
raise Http404('Invalid file path')
with open(path, 'rb') as f:
content = f.read()
return HttpResponse(content, content_type='application/pdf')
The second example demonstrates how to handle filenames when you must accept them as part of the signed payload. In this pattern, the signature covers a canonicalized, validated filename that is stripped of traversal components and restricted to a safe character set.
import re
from django.core.signing import TimestampSigner, BadSignature
from django.http import HttpResponse, HttpResponseBadRequest
from django.views import View
signer = TimestampSigner()
SAFE_FILENAME_RE = re.compile(r'^[A-Za-z0-9._\-]+$')
def make_signed_token(filename: str) -> str:
if not SAFE_FILENAME_RE.match(filename):
raise ValueError('Invalid filename')
return signer.sign(filename)
class SafeFileServeView(View):
def get(self, request, signed_token):
try:
filename = signer.unsign(signed_token, max_age=3600)
except BadSignature:
return HttpResponseBadRequest('Invalid signature')
if not SAFE_FILENAME_RE.match(filename):
return HttpResponseBadRequest('Invalid filename')
base = '/var/www/uploads'
# Use pathlib to resolve and ensure containment
from pathlib import Path
resolved = (Path(base) / filename).resolve()
if not str(resolved).startswith(str(Path(base).resolve())):
return HttpResponseBadRequest('Path traversal detected')
try:
with open(resolved, 'rb') as f:
return HttpResponse(f.read(), content_type='application/octet-stream')
except FileNotFoundError:
return HttpResponseBadRequest('File not found')
Key principles across both patterns include validating input before signing or verifying after canonicalization, using an allowlist for filenames, resolving paths with os.path.realpath or Path.resolve(), and confirming the resolved path remains within the intended directory. Avoid signing raw user input that includes path components, and never rely on the signature alone to prevent directory traversal. MiddleBrick’s scans can highlight endpoints where signatures are applied after path construction or where unsafe filename concatenation occurs, helping you prioritize remediation.
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |