{"record":{"id":"3567f64037de360c","repo":"django/django","slug":"detected-path-traversal-attempt-in-s","errorCode":null,"errorMessage":"Detected path traversal attempt in '%s'","messagePattern":"Detected path traversal attempt in '(.+?)'","errorType":"validation","errorClass":"SuspiciousFileOperation","httpStatus":400,"severity":"critical","filePath":"django/core/files/storage/base.py","lineNumber":83,"sourceCode":"        return get_valid_filename(name)\n\n    def get_alternative_name(self, file_root, file_ext):\n        \"\"\"\n        Return an alternative filename, by adding an underscore and a random 7\n        character alphanumeric string (before the file extension, if one\n        exists) to the filename.\n        \"\"\"\n        return \"%s_%s%s\" % (file_root, get_random_string(7), file_ext)\n\n    def get_available_name(self, name, max_length=None):\n        \"\"\"\n        Return a filename that's free on the target storage system and\n        available for new content to be written to.\n        \"\"\"\n        name = str(name).replace(\"\\\\\", \"/\")\n        dir_name, file_name = os.path.split(name)\n        if \"..\" in pathlib.PurePath(dir_name).parts:\n            raise SuspiciousFileOperation(\n                \"Detected path traversal attempt in '%s'\" % dir_name\n            )\n        validate_file_name(file_name)\n        file_ext = \"\".join(pathlib.PurePath(file_name).suffixes)\n        file_root = file_name.removesuffix(file_ext)\n        # If the filename is not available, generate an alternative\n        # filename until one is available.\n        # Truncate original name if required, so the new filename does not\n        # exceed the max_length.\n        while not self.is_name_available(name, max_length=max_length):\n            # file_ext includes the dot.\n            name = os.path.join(\n                dir_name, self.get_alternative_name(file_root, file_ext)\n            )\n            if max_length is None:\n                continue\n            # Truncate file_root if max_length exceeded.\n            truncation = len(name) - max_length","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/django/django/blob/ae25a40be07e8a749edf526df37c93e59d4a22c9/django/core/files/storage/base.py#L65-L101","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Sanitize user-supplied filenames with get_valid_filename() before save().","Make upload_to callables return plain names with no directory components.","Strip '..' and leading slashes from incoming filenames at the trust boundary.","Catch SuspiciousFileOperation and reject the upload with a 400 response."],"exampleFix":"// before\nstorage.save(request.POST['filename'], upload)  # ../x -> blocked\n// after\nfrom django.utils.text import get_valid_filename\nstorage.save(get_valid_filename(request.POST['filename']), upload)","handlingStrategy":"validation","validationCode":"import pathlib\nfrom django.utils.text import get_valid_filename\n\ndef safe_storage_name(name):\n    name = str(name).replace('\\\\', '/')\n    path = pathlib.PurePosixPath(name)\n    if path.is_absolute() or '..' in path.parts:\n        raise ValueError(f'Unsafe path: {name}')\n    return get_valid_filename(name)","typeGuard":"import pathlib\n\ndef is_safe_relative_path(name):\n    path = pathlib.PurePosixPath(str(name).replace('\\\\', '/'))\n    return not path.is_absolute() and '..' not in path.parts","tryCatchPattern":"from django.core.exceptions import SuspiciousFileOperation\n\ntry:\n    storage.save(name, f)\nexcept SuspiciousFileOperation:\n    # reject upload with a 400\n    raise PermissionError('Invalid filename')","preventionTips":["Run user-supplied filenames through get_valid_filename() before save.","Make upload_to callables return plain names without '..' segments.","Never trust client-provided paths; use os.path.basename on segments.","Catch SuspiciousFileOperation at the view boundary and return 400."],"tags":["security","storage","path-traversal","suspiciousfileoperation"],"analyzedSha":"ae25a40be07e8a749edf526df37c93e59d4a22c9","analyzedAt":"2026-08-06T21:46:51.801Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}