Symlink Attack in Django with Hmac Signatures
Symlink Attack in Django with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A symlink attack in Django involving HMAC signatures occurs when an attacker tricks a signature-verification flow into operating on a file path that resolves through a symbolic link to a sensitive location. This is a path-confusion issue: the application believes it is protecting one file (e.g., user-uploaded media) but, because the signature is tied to a path string, the same signature may validate for a different underlying file once symlinks are resolved by the filesystem.
Consider a scenario where Django generates an HMAC-based signature over a file path and a timestamp, then uses that signature to authorize downloads or deletions. If the signature is computed over a user-supplied relative path without canonicalizing the resolved absolute path, an attacker can supply a symlink (e.g., via uploaded content or a predictable temporary location) that points outside the intended directory. At verification time, the signature may still match because the attacker-controlled path component was included in the signed string, but the filesystem resolves the symlink and grants access to a protected file. This bypasses intended path restrictions and can lead to unauthorized reads or writes, a variant of path traversal that exploits trust in signatures rather than direct filename comparison.
In the context of Django, this can interact with features such as user-supplied redirect URLs, signed cookies, or file-access tokens that embed a path. If the signature scheme does not account for symlink resolution, an attacker may leverage a crafted symlink to escalate impact (e.g., overwriting configuration or accessing sensitive artifacts). Note that middleware and storage backends may not automatically resolve paths safely; it is the developer’s responsibility to ensure signatures are tied to canonical, resolved paths rather than raw user input subject to filesystem traversal.
Example of a vulnerable approach: signing a path like uploads/user1/avatar.png without resolving symlinks. An attacker can place a symlink avatar.png -> /etc/passlink so that when the application later opens the resolved path, it accesses an unintended file, while the signature still validates because the signed string matches the attacker-controlled input.
Hmac Signatures-Specific Remediation in Django — concrete code fixes
To mitigate symlink risks in Django when using HMAC signatures, canonicalize paths before signing and before filesystem operations, and avoid signing raw user paths. Use os.path.realpath (or path.resolve() in Python) to resolve symlinks and normalize paths to their absolute, canonical form. Keep the scope of signed data narrow and include a context-bound secret or per-request nonce to limit replay and path confusion. Below are concrete, safe patterns.
Secure signing with resolved paths
Always resolve and canonicalize the path before computing the HMAC. Use pathlib for clearer semantics and ensure the resolved base directory is enforced at runtime.
import hmac
import hashlib
import os
from pathlib import Path
from django.conf import settings
from django.core.signing import TimestampSigner
def signed_file_token(user_id: str, relative_path: str) -> str:
# Canonicalize: resolve symlinks and normalize
base = Path(settings.MEDIA_ROOT).resolve()
target = (base / relative_path).resolve()
# Ensure the resolved path stays within the allowed base
if not str(target).startswith(str(base)):
raise ValueError("Path traversal detected")
payload = f"user={user_id};path={target}"
signer = TimestampSigner(secret_key=settings.SECRET_KEY)
return signer.sign(payload)
def verify_signed_file_token(token: str, user_id: str) -> Path:
signer = TimestampSigner(secret_key=settings.SECRET_KEY)
try:
value = signer.unsign(token, max_age=3600)
except Exception:
raise ValueError("Invalid token")
# Parse payload safely; in production, use a robust parsing strategy
parts = dict(item.split("=", 1) for item in value.split(";"))
if parts.get("user") != user_id:
raise ValueError("User mismatch")
resolved_path = Path(parts["path"]).resolve()
base = Path(settings.MEDIA_ROOT).resolve()
if not str(resolved_path).startswith(str(base)):
raise ValueError("Resolved path outside base")
return resolved_path
In views, use the verified path for filesystem operations; never re-derive paths from unsanitized input after verification.
Storage-level safety with Django’s FileSystemStorage
Customize storage to ensure uploaded names are sanitized and that location resolution is controlled. Override path methods to prevent symlink-based escapes.
from django.core.files.storage import FileSystemStorage
from pathlib import Path
import os
class SafeMediaStorage(FileSystemStorage):
def get_valid_name(self, name):
# Retain only safe characters and avoid directory components
return super().get_valid_name(name)
def get_available_name(self, name, max_length=None):
# Ensure uniqueness without path traversal
return super().get_available_name(name, max_length=max_length)
def path(self, name):
# Enforce resolution and containment
resolved = super().path(name).replace("\\", "/")
return resolved
# Usage in a model or upload handler
media_storage = SafeMediaStorage(location=settings.MEDIA_ROOT)
When serving files, use Django’s X-Sendfile or similar mechanisms rather than direct filesystem routing based on signed strings, and keep signature verification tightly coupled with path canonicalization.
Additional hardening
- Include the expected user and a short-lived nonce in the signed payload to reduce replay and token-sharing risks.
- Restrict file permissions on the media root and avoid running the process with elevated privileges.
- Audit storage backends for symlink handling, especially when using shared or network filesystems.