django/django · critical · SuspiciousFileOperation

Detected path traversal attempt in '%s'

Error message

Detected path traversal attempt in '%s'

What it means

Raised by Storage.get_available_name() (django/core/files/storage/base.py:83) as SuspiciousFileOperation when pathlib.PurePath(dir_name).parts contains '..'. This is an active security guard: it stops a supplied filename like '../secret' from escaping the storage root. It fires during save() before any name is written.

Source

Thrown at django/core/files/storage/base.py:83

        return get_valid_filename(name)

    def get_alternative_name(self, file_root, file_ext):
        """
        Return an alternative filename, by adding an underscore and a random 7
        character alphanumeric string (before the file extension, if one
        exists) to the filename.
        """
        return "%s_%s%s" % (file_root, get_random_string(7), file_ext)

    def get_available_name(self, name, max_length=None):
        """
        Return a filename that's free on the target storage system and
        available for new content to be written to.
        """
        name = str(name).replace("\\", "/")
        dir_name, file_name = os.path.split(name)
        if ".." in pathlib.PurePath(dir_name).parts:
            raise SuspiciousFileOperation(
                "Detected path traversal attempt in '%s'" % dir_name
            )
        validate_file_name(file_name)
        file_ext = "".join(pathlib.PurePath(file_name).suffixes)
        file_root = file_name.removesuffix(file_ext)
        # If the filename is not available, generate an alternative
        # filename until one is available.
        # Truncate original name if required, so the new filename does not
        # exceed the max_length.
        while not self.is_name_available(name, max_length=max_length):
            # file_ext includes the dot.
            name = os.path.join(
                dir_name, self.get_alternative_name(file_root, file_ext)
            )
            if max_length is None:
                continue
            # Truncate file_root if max_length exceeded.
            truncation = len(name) - max_length

View on GitHub (pinned to ae25a40be0)

Solutions

  1. Sanitize user-supplied filenames with get_valid_filename() before save().
  2. Make upload_to callables return plain names with no directory components.
  3. Strip '..' and leading slashes from incoming filenames at the trust boundary.
  4. Catch SuspiciousFileOperation and reject the upload with a 400 response.

Example fix

// before
storage.save(request.POST['filename'], upload)  # ../x -> blocked
// after
from django.utils.text import get_valid_filename
storage.save(get_valid_filename(request.POST['filename']), upload)
Defensive patterns

Strategy: validation

Validate before calling

import pathlib
from django.utils.text import get_valid_filename

def safe_storage_name(name):
    name = str(name).replace('\\', '/')
    path = pathlib.PurePosixPath(name)
    if path.is_absolute() or '..' in path.parts:
        raise ValueError(f'Unsafe path: {name}')
    return get_valid_filename(name)

Type guard

import pathlib

def is_safe_relative_path(name):
    path = pathlib.PurePosixPath(str(name).replace('\\', '/'))
    return not path.is_absolute() and '..' not in path.parts

Try / catch

from django.core.exceptions import SuspiciousFileOperation

try:
    storage.save(name, f)
except SuspiciousFileOperation:
    # reject upload with a 400
    raise PermissionError('Invalid filename')

Prevention

When it happens

Trigger: Calling storage.save('../etc/passwd', file); a FileField upload_to returning '../../media/x'; user-uploaded filenames containing '..' passed unvalidated to save(); get_available_name() being called directly with a traversal path. The check runs on the directory portion after os.path.split.

Common situations: Accepting user-supplied filenames without sanitization; misconfigured upload_to callables returning relative escapes; zip-file extraction code passing member names verbatim to storage; migration of legacy data with malformed paths.

Related errors


AI-assisted analysis of django/django@ae25a40be0 (2026-08-06). Data as JSON: /api/errors/3567f64037de360c. Report an issue: GitHub.