HIGH symlink attackdjangohmac signatures

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.

Frequently Asked Questions

Why is resolving paths with os.path.realpath or path.resolve() important for HMAC signatures in Django?
Because it resolves symlinks and normalizes the absolute path, ensuring the signature is bound to the canonical file location rather than a user-supplied path that may traverse via symlinks.
Can I safely include the full file path in HMAC signed data in Django?
Include enough context to bind the signature (e.g., user ID and a resolved, canonical path), but avoid exposing sensitive directory structures in URLs; prefer opaque tokens mapped server-side to canonical paths.