VYPR
High severityNVD Advisory· Published Aug 26, 2014· Updated May 6, 2026

CVE-2014-0481

CVE-2014-0481

Description

The default configuration for the file upload handling system in Django before 1.4.14, 1.5.x before 1.5.9, 1.6.x before 1.6.6, and 1.7 before release candidate 3 uses a sequential file name generation process when a file with a conflicting name is uploaded, which allows remote attackers to cause a denial of service (CPU consumption) by unloading a multiple files with the same name.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
DjangoPyPI
< 1.4.141.4.14
DjangoPyPI
>= 1.5, < 1.5.91.5.9
DjangoPyPI
>= 1.6, < 1.6.61.6.6

Affected products

43
  • cpe:2.3:a:djangoproject:django:*:*:*:*:*:*:*:*+ 39 more
    • cpe:2.3:a:djangoproject:django:*:*:*:*:*:*:*:*range: <=1.4.13
    • cpe:2.3:a:djangoproject:django:1.4:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.1:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.10:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.11:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.12:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.2:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.4:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.5:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.6:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.7:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.8:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.4.9:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5.1:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5.2:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5.3:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5.4:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5.5:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5.6:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5.7:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5.8:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5:alpha:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.5:beta:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6:-:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6.1:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6.2:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6.3:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6.4:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6.5:*:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6:beta1:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6:beta2:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6:beta3:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.6:beta4:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.7:beta1:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.7:beta2:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.7:beta3:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.7:beta4:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.7:rc1:*:*:*:*:*:*
    • cpe:2.3:a:djangoproject:django:1.7:rc2:*:*:*:*:*:*
  • cpe:2.3:o:debian:debian_linux:7.0:*:*:*:*:*:*:*
  • OpenSUSE/openSUSE2 versions
    cpe:2.3:o:opensuse:opensuse:13.1:*:*:*:*:*:*:*+ 1 more
    • cpe:2.3:o:opensuse:opensuse:13.1:*:*:*:*:*:*:*
    • cpe:2.3:o:opensuse_project:opensuse:12.3:*:*:*:*:*:*:*

Patches

3
30042d475bf0

[1.4.x] Fixed #23157 -- Removed O(n) algorithm when uploading duplicate file names.

https://github.com/django/djangoTim GrahamAug 8, 2014via ghsa
6 files changed · +75 28
  • django/core/files/storage.py+5 6 modified
    @@ -1,13 +1,13 @@
     import os
     import errno
     import urlparse
    -import itertools
     from datetime import datetime
     
     from django.conf import settings
     from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
     from django.core.files import locks, File
     from django.core.files.move import file_move_safe
    +from django.utils.crypto import get_random_string
     from django.utils.encoding import force_unicode, filepath_to_uri
     from django.utils.functional import LazyObject
     from django.utils.importlib import import_module
    @@ -63,13 +63,12 @@ def get_available_name(self, name):
             """
             dir_name, file_name = os.path.split(name)
             file_root, file_ext = os.path.splitext(file_name)
    -        # If the filename already exists, add an underscore and a number (before
    -        # the file extension, if one exists) to the filename until the generated
    -        # filename doesn't exist.
    -        count = itertools.count(1)
    +        # If the filename already exists, add an underscore and a random 7
    +        # character alphanumeric string (before the file extension, if one
    +        # exists) to the filename until the generated filename doesn't exist.
             while self.exists(name):
                 # file_ext includes the dot.
    -            name = os.path.join(dir_name, "%s_%s%s" % (file_root, count.next(), file_ext))
    +            name = os.path.join(dir_name, "%s_%s%s" % (file_root, get_random_string(7), file_ext))
     
             return name
     
    
  • docs/howto/custom-file-storage.txt+10 2 modified
    @@ -86,5 +86,13 @@ the provided filename into account. The ``name`` argument passed to this method
     will have already cleaned to a filename valid for the storage system, according
     to the ``get_valid_name()`` method described above.
     
    -The code provided on ``Storage`` simply appends ``"_1"``, ``"_2"``, etc. to the
    -filename until it finds one that's available in the destination directory.
    +.. versionchanged:: 1.4.14
    +
    +    If a file with ``name`` already exists, an underscore plus a random 7
    +    character alphanumeric string is appended to the filename before the
    +    extension.
    +
    +    Previously, an underscore followed by a number (e.g. ``"_1"``, ``"_2"``,
    +    etc.) was appended to the filename until an avaible name in the destination
    +    directory was found. A malicious user could exploit this deterministic
    +    algorithm to create a denial-of-service attack.
    
  • docs/ref/files/storage.txt+13 3 modified
    @@ -18,7 +18,7 @@ Django provides two convenient ways to access the current storage class:
     .. function:: get_storage_class([import_path=None])
     
         Returns a class or module which implements the storage API.
    -    
    +
         When called without the ``import_path`` parameter ``get_storage_class``
         will return the current default storage system as defined by
         :setting:`DEFAULT_FILE_STORAGE`. If ``import_path`` is provided,
    @@ -35,9 +35,9 @@ The FileSystemStorage Class
         basic file storage on a local filesystem. It inherits from
         :class:`~django.core.files.storage.Storage` and provides implementations
         for all the public methods thereof.
    -    
    +
         .. note::
    -    
    +
             The :class:`FileSystemStorage.delete` method will not raise
             raise an exception if the given file name does not exist.
     
    @@ -85,6 +85,16 @@ The Storage Class
             available for new content to be written to on the target storage
             system.
     
    +        .. versionchanged:: 1.4.14
    +
    +        If a file with ``name`` already exists, an underscore plus a random 7
    +        character alphanumeric string is appended to the filename before the
    +        extension.
    +
    +        Previously, an underscore followed by a number (e.g. ``"_1"``, ``"_2"``,
    +        etc.) was appended to the filename until an avaible name in the
    +        destination directory was found. A malicious user could exploit this
    +        deterministic algorithm to create a denial-of-service attack.
     
         .. method:: get_valid_name(name)
     
    
  • docs/releases/1.4.14.txt+20 0 modified
    @@ -18,3 +18,23 @@ To remedy this, URL reversing now ensures that no URL starts with two slashes
     (//), replacing the second slash with its URL encoded counterpart (%2F). This
     approach ensures that semantics stay the same, while making the URL relative to
     the domain and not to the scheme.
    +
    +File upload denial-of-service
    +=============================
    +
    +Before this release, Django's file upload handing in its default configuration
    +may degrade to producing a huge number of ``os.stat()`` system calls when a
    +duplicate filename is uploaded. Since ``stat()`` may invoke IO, this may produce
    +a huge data-dependent slowdown that slowly worsens over time. The net result is
    +that given enough time, a user with the ability to upload files can cause poor
    +performance in the upload handler, eventually causing it to become very slow
    +simply by uploading 0-byte files. At this point, even a slow network connection
    +and few HTTP requests would be all that is necessary to make a site unavailable.
    +
    +We've remedied the issue by changing the algorithm for generating file names
    +if a file with the uploaded name already exists.
    +:meth:`Storage.get_available_name()
    +<django.core.files.storage.Storage.get_available_name>` now appends an
    +underscore plus a random 7 character alphanumeric string (e.g. ``"_x3a1gho"``),
    +rather than iterating through an underscore followed by a number (e.g. ``"_1"``,
    +``"_2"``, etc.).
    
  • tests/modeltests/files/tests.py+13 8 modified
    @@ -8,10 +8,14 @@
     from django.core.files.base import ContentFile
     from django.core.files.uploadedfile import SimpleUploadedFile
     from django.test import TestCase
    +from django.utils import six
     
     from .models import Storage, temp_storage, temp_storage_location
     
     
    +FILE_SUFFIX_REGEX = '[A-Za-z0-9]{7}'
    +
    +
     class FileTests(TestCase):
         def tearDown(self):
             shutil.rmtree(temp_storage_location)
    @@ -57,27 +61,28 @@ def test_files(self):
             # Save another file with the same name.
             obj2 = Storage()
             obj2.normal.save("django_test.txt", ContentFile("more content"))
    -        self.assertEqual(obj2.normal.name, "tests/django_test_1.txt")
    +        obj2_name = obj2.normal.name
    +        six.assertRegex(self, obj2_name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
             self.assertEqual(obj2.normal.size, 12)
     
             # Push the objects into the cache to make sure they pickle properly
             cache.set("obj1", obj1)
             cache.set("obj2", obj2)
    -        self.assertEqual(cache.get("obj2").normal.name, "tests/django_test_1.txt")
    +        six.assertRegex(self, cache.get("obj2").normal.name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
     
             # Deleting an object does not delete the file it uses.
             obj2.delete()
             obj2.normal.save("django_test.txt", ContentFile("more content"))
    -        self.assertEqual(obj2.normal.name, "tests/django_test_2.txt")
    +        self.assertNotEqual(obj2_name, obj2.normal.name)
    +        six.assertRegex(self, obj2.normal.name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
     
             # Multiple files with the same name get _N appended to them.
    -        objs = [Storage() for i in range(3)]
    +        objs = [Storage() for i in range(2)]
             for o in objs:
                 o.normal.save("multiple_files.txt", ContentFile("Same Content"))
    -        self.assertEqual(
    -            [o.normal.name for o in objs],
    -            ["tests/multiple_files.txt", "tests/multiple_files_1.txt", "tests/multiple_files_2.txt"]
    -        )
    +        names = [o.normal.name for o in objs]
    +        self.assertEqual(names[0], "tests/multiple_files.txt")
    +        six.assertRegex(self, names[1], "tests/multiple_files_%s.txt" % FILE_SUFFIX_REGEX)
             for o in objs:
                 o.delete()
     
    
  • tests/regressiontests/file_storage/tests.py+14 9 modified
    @@ -23,7 +23,7 @@
     from django.core.files.storage import FileSystemStorage, get_storage_class
     from django.core.files.uploadedfile import UploadedFile
     from django.test import SimpleTestCase
    -from django.utils import unittest
    +from django.utils import six, unittest
     
     # Try to import PIL in either of the two ways it can end up installed.
     # Checking for the existence of Image is enough for CPython, but
    @@ -37,6 +37,9 @@
             Image = None
     
     
    +FILE_SUFFIX_REGEX = '[A-Za-z0-9]{7}'
    +
    +
     class GetStorageClassTests(SimpleTestCase):
     
         def test_get_filesystem_storage(self):
    @@ -417,10 +420,9 @@ def test_race_condition(self):
             self.thread.start()
             name = self.save_file('conflict')
             self.thread.join()
    -        self.assertTrue(self.storage.exists('conflict'))
    -        self.assertTrue(self.storage.exists('conflict_1'))
    -        self.storage.delete('conflict')
    -        self.storage.delete('conflict_1')
    +        files = sorted(os.listdir(self.storage_dir))
    +        self.assertEqual(files[0], 'conflict')
    +        six.assertRegex(self, files[1], 'conflict_%s' % FILE_SUFFIX_REGEX)
     
     class FileStoragePermissions(unittest.TestCase):
         def setUp(self):
    @@ -457,9 +459,10 @@ def test_directory_with_dot(self):
             self.storage.save('dotted.path/test', ContentFile("1"))
             self.storage.save('dotted.path/test', ContentFile("2"))
     
    +        files = sorted(os.listdir(os.path.join(self.storage_dir, 'dotted.path')))
             self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path')))
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test')))
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_1')))
    +        self.assertEqual(files[0], 'test')
    +        six.assertRegex(self, files[1], 'test_%s' % FILE_SUFFIX_REGEX)
     
         def test_first_character_dot(self):
             """
    @@ -472,10 +475,12 @@ def test_first_character_dot(self):
             self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test')))
             # Before 2.6, a leading dot was treated as an extension, and so
             # underscore gets added to beginning instead of end.
    +        files = sorted(os.listdir(os.path.join(self.storage_dir, 'dotted.path')))
    +        self.assertEqual(files[0], '.test')
             if sys.version_info < (2, 6):
    -            self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/_1.test')))
    +            six.assertRegex(self, files[1], '_%s.test' % FILE_SUFFIX_REGEX)
             else:
    -            self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_1')))
    +            six.assertRegex(self, files[1], '.test_%s' % FILE_SUFFIX_REGEX)
     
     class DimensionClosingBug(unittest.TestCase):
         """
    
26cd48e166ac

[1.5.x] Fixed #23157 -- Removed O(n) algorithm when uploading duplicate file names.

https://github.com/django/djangoTim GrahamAug 8, 2014via ghsa
7 files changed · +93 25
  • django/core/files/storage.py+5 6 modified
    @@ -4,13 +4,13 @@
         from urllib.parse import urljoin
     except ImportError:     # Python 2
         from urlparse import urljoin
    -import itertools
     from datetime import datetime
     
     from django.conf import settings
     from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
     from django.core.files import locks, File
     from django.core.files.move import file_move_safe
    +from django.utils.crypto import get_random_string
     from django.utils.encoding import force_text, filepath_to_uri
     from django.utils.functional import LazyObject
     from django.utils.importlib import import_module
    @@ -66,13 +66,12 @@ def get_available_name(self, name):
             """
             dir_name, file_name = os.path.split(name)
             file_root, file_ext = os.path.splitext(file_name)
    -        # If the filename already exists, add an underscore and a number (before
    -        # the file extension, if one exists) to the filename until the generated
    -        # filename doesn't exist.
    -        count = itertools.count(1)
    +        # If the filename already exists, add an underscore and a random 7
    +        # character alphanumeric string (before the file extension, if one
    +        # exists) to the filename until the generated filename doesn't exist.
             while self.exists(name):
                 # file_ext includes the dot.
    -            name = os.path.join(dir_name, "%s_%s%s" % (file_root, next(count), file_ext))
    +            name = os.path.join(dir_name, "%s_%s%s" % (file_root, get_random_string(7), file_ext))
     
             return name
     
    
  • docs/howto/custom-file-storage.txt+11 2 modified
    @@ -83,5 +83,14 @@ the provided filename into account. The ``name`` argument passed to this method
     will have already cleaned to a filename valid for the storage system, according
     to the ``get_valid_name()`` method described above.
     
    -The code provided on ``Storage`` simply appends ``"_1"``, ``"_2"``, etc. to the
    -filename until it finds one that's available in the destination directory.
    +.. versionchanged:: 1.5.9
    +
    +    If a file with ``name`` already exists, an underscore plus a random 7
    +    character alphanumeric string is appended to the filename before the
    +    extension.
    +
    +    Previously, an underscore followed by a number (e.g. ``"_1"``, ``"_2"``,
    +    etc.) was appended to the filename until an avaible name in the destination
    +    directory was found. A malicious user could exploit this deterministic
    +    algorithm to create a denial-of-service attack. This change was also made
    +    in Django 1.4.14.
    
  • docs/ref/files/storage.txt+11 0 modified
    @@ -81,6 +81,17 @@ The Storage Class
             available for new content to be written to on the target storage
             system.
     
    +        .. versionchanged:: 1.5.9
    +
    +        If a file with ``name`` already exists, an underscore plus a random 7
    +        character alphanumeric string is appended to the filename before the
    +        extension.
    +
    +        Previously, an underscore followed by a number (e.g. ``"_1"``, ``"_2"``,
    +        etc.) was appended to the filename until an avaible name in the
    +        destination directory was found. A malicious user could exploit this
    +        deterministic algorithm to create a denial-of-service attack. This
    +        change was also made in Django 1.4.14.
     
         .. method:: get_valid_name(name)
     
    
  • docs/releases/1.4.14.txt+20 0 modified
    @@ -18,3 +18,23 @@ To remedy this, URL reversing now ensures that no URL starts with two slashes
     (//), replacing the second slash with its URL encoded counterpart (%2F). This
     approach ensures that semantics stay the same, while making the URL relative to
     the domain and not to the scheme.
    +
    +File upload denial-of-service
    +=============================
    +
    +Before this release, Django's file upload handing in its default configuration
    +may degrade to producing a huge number of ``os.stat()`` system calls when a
    +duplicate filename is uploaded. Since ``stat()`` may invoke IO, this may produce
    +a huge data-dependent slowdown that slowly worsens over time. The net result is
    +that given enough time, a user with the ability to upload files can cause poor
    +performance in the upload handler, eventually causing it to become very slow
    +simply by uploading 0-byte files. At this point, even a slow network connection
    +and few HTTP requests would be all that is necessary to make a site unavailable.
    +
    +We've remedied the issue by changing the algorithm for generating file names
    +if a file with the uploaded name already exists.
    +:meth:`Storage.get_available_name()
    +<django.core.files.storage.Storage.get_available_name>` now appends an
    +underscore plus a random 7 character alphanumeric string (e.g. ``"_x3a1gho"``),
    +rather than iterating through an underscore followed by a number (e.g. ``"_1"``,
    +``"_2"``, etc.).
    
  • docs/releases/1.5.9.txt+20 0 modified
    @@ -18,3 +18,23 @@ To remedy this, URL reversing now ensures that no URL starts with two slashes
     (//), replacing the second slash with its URL encoded counterpart (%2F). This
     approach ensures that semantics stay the same, while making the URL relative to
     the domain and not to the scheme.
    +
    +File upload denial-of-service
    +=============================
    +
    +Before this release, Django's file upload handing in its default configuration
    +may degrade to producing a huge number of ``os.stat()`` system calls when a
    +duplicate filename is uploaded. Since ``stat()`` may invoke IO, this may produce
    +a huge data-dependent slowdown that slowly worsens over time. The net result is
    +that given enough time, a user with the ability to upload files can cause poor
    +performance in the upload handler, eventually causing it to become very slow
    +simply by uploading 0-byte files. At this point, even a slow network connection
    +and few HTTP requests would be all that is necessary to make a site unavailable.
    +
    +We've remedied the issue by changing the algorithm for generating file names
    +if a file with the uploaded name already exists.
    +:meth:`Storage.get_available_name()
    +<django.core.files.storage.Storage.get_available_name>` now appends an
    +underscore plus a random 7 character alphanumeric string (e.g. ``"_x3a1gho"``),
    +rather than iterating through an underscore followed by a number (e.g. ``"_1"``,
    +``"_2"``, etc.).
    
  • tests/modeltests/files/tests.py+13 9 modified
    @@ -9,11 +9,14 @@
     from django.core.files.base import ContentFile
     from django.core.files.uploadedfile import SimpleUploadedFile
     from django.test import TestCase
    -from django.utils import unittest
    +from django.utils import six, unittest
     
     from .models import Storage, temp_storage, temp_storage_location
     
     
    +FILE_SUFFIX_REGEX = '[A-Za-z0-9]{7}'
    +
    +
     class FileStorageTests(TestCase):
         def tearDown(self):
             shutil.rmtree(temp_storage_location)
    @@ -59,27 +62,28 @@ def test_files(self):
             # Save another file with the same name.
             obj2 = Storage()
             obj2.normal.save("django_test.txt", ContentFile("more content"))
    -        self.assertEqual(obj2.normal.name, "tests/django_test_1.txt")
    +        obj2_name = obj2.normal.name
    +        six.assertRegex(self, obj2_name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
             self.assertEqual(obj2.normal.size, 12)
     
             # Push the objects into the cache to make sure they pickle properly
             cache.set("obj1", obj1)
             cache.set("obj2", obj2)
    -        self.assertEqual(cache.get("obj2").normal.name, "tests/django_test_1.txt")
    +        six.assertRegex(self, cache.get("obj2").normal.name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
     
             # Deleting an object does not delete the file it uses.
             obj2.delete()
             obj2.normal.save("django_test.txt", ContentFile("more content"))
    -        self.assertEqual(obj2.normal.name, "tests/django_test_2.txt")
    +        self.assertNotEqual(obj2_name, obj2.normal.name)
    +        six.assertRegex(self, obj2.normal.name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
     
             # Multiple files with the same name get _N appended to them.
    -        objs = [Storage() for i in range(3)]
    +        objs = [Storage() for i in range(2)]
             for o in objs:
                 o.normal.save("multiple_files.txt", ContentFile("Same Content"))
    -        self.assertEqual(
    -            [o.normal.name for o in objs],
    -            ["tests/multiple_files.txt", "tests/multiple_files_1.txt", "tests/multiple_files_2.txt"]
    -        )
    +        names = [o.normal.name for o in objs]
    +        self.assertEqual(names[0], "tests/multiple_files.txt")
    +        six.assertRegex(self, names[1], "tests/multiple_files_%s.txt" % FILE_SUFFIX_REGEX)
             for o in objs:
                 o.delete()
     
    
  • tests/regressiontests/file_storage/tests.py+13 8 modified
    @@ -40,6 +40,9 @@
             Image = None
     
     
    +FILE_SUFFIX_REGEX = '[A-Za-z0-9]{7}'
    +
    +
     class GetStorageClassTests(SimpleTestCase):
     
         def test_get_filesystem_storage(self):
    @@ -431,10 +434,9 @@ def test_race_condition(self):
             self.thread.start()
             name = self.save_file('conflict')
             self.thread.join()
    -        self.assertTrue(self.storage.exists('conflict'))
    -        self.assertTrue(self.storage.exists('conflict_1'))
    -        self.storage.delete('conflict')
    -        self.storage.delete('conflict_1')
    +        files = sorted(os.listdir(self.storage_dir))
    +        self.assertEqual(files[0], 'conflict')
    +        six.assertRegex(self, files[1], 'conflict_%s' % FILE_SUFFIX_REGEX)
     
     @unittest.skipIf(sys.platform.startswith('win'), "Windows only partially supports umasks and chmod.")
     class FileStoragePermissions(unittest.TestCase):
    @@ -478,9 +480,10 @@ def test_directory_with_dot(self):
             self.storage.save('dotted.path/test', ContentFile("1"))
             self.storage.save('dotted.path/test', ContentFile("2"))
     
    +        files = sorted(os.listdir(os.path.join(self.storage_dir, 'dotted.path')))
             self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path')))
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test')))
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_1')))
    +        self.assertEqual(files[0], 'test')
    +        six.assertRegex(self, files[1], 'test_%s' % FILE_SUFFIX_REGEX)
     
         def test_first_character_dot(self):
             """
    @@ -490,8 +493,10 @@ def test_first_character_dot(self):
             self.storage.save('dotted.path/.test', ContentFile("1"))
             self.storage.save('dotted.path/.test', ContentFile("2"))
     
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test')))
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_1')))
    +        files = sorted(os.listdir(os.path.join(self.storage_dir, 'dotted.path')))
    +        self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path')))
    +        self.assertEqual(files[0], '.test')
    +        six.assertRegex(self, files[1], '.test_%s' % FILE_SUFFIX_REGEX)
     
     class DimensionClosingBug(unittest.TestCase):
         """
    
dd0c3f4ee1a3

[1.6.x] Fixed #23157 -- Removed O(n) algorithm when uploading duplicate file names.

https://github.com/django/djangoTim GrahamAug 8, 2014via ghsa
8 files changed · +113 25
  • django/core/files/storage.py+5 6 modified
    @@ -1,12 +1,12 @@
     import os
     import errno
    -import itertools
     from datetime import datetime
     
     from django.conf import settings
     from django.core.exceptions import SuspiciousFileOperation
     from django.core.files import locks, File
     from django.core.files.move import file_move_safe
    +from django.utils.crypto import get_random_string
     from django.utils.encoding import force_text, filepath_to_uri
     from django.utils.functional import LazyObject
     from django.utils.module_loading import import_by_path
    @@ -67,13 +67,12 @@ def get_available_name(self, name):
             """
             dir_name, file_name = os.path.split(name)
             file_root, file_ext = os.path.splitext(file_name)
    -        # If the filename already exists, add an underscore and a number (before
    -        # the file extension, if one exists) to the filename until the generated
    -        # filename doesn't exist.
    -        count = itertools.count(1)
    +        # If the filename already exists, add an underscore and a random 7
    +        # character alphanumeric string (before the file extension, if one
    +        # exists) to the filename until the generated filename doesn't exist.
             while self.exists(name):
                 # file_ext includes the dot.
    -            name = os.path.join(dir_name, "%s_%s%s" % (file_root, next(count), file_ext))
    +            name = os.path.join(dir_name, "%s_%s%s" % (file_root, get_random_string(7), file_ext))
     
             return name
     
    
  • docs/howto/custom-file-storage.txt+11 2 modified
    @@ -83,5 +83,14 @@ the provided filename into account. The ``name`` argument passed to this method
     will have already cleaned to a filename valid for the storage system, according
     to the ``get_valid_name()`` method described above.
     
    -The code provided on ``Storage`` simply appends ``"_1"``, ``"_2"``, etc. to the
    -filename until it finds one that's available in the destination directory.
    +.. versionchanged:: 1.6.6
    +
    +    If a file with ``name`` already exists, an underscore plus a random 7
    +    character alphanumeric string is appended to the filename before the
    +    extension.
    +
    +    Previously, an underscore followed by a number (e.g. ``"_1"``, ``"_2"``,
    +    etc.) was appended to the filename until an avaible name in the destination
    +    directory was found. A malicious user could exploit this deterministic
    +    algorithm to create a denial-of-service attack. This change was also made
    +    in Django 1.5.9 and 1.4.14.
    
  • docs/ref/files/storage.txt+11 0 modified
    @@ -81,6 +81,17 @@ The Storage Class
             available for new content to be written to on the target storage
             system.
     
    +        .. versionchanged:: 1.6.6
    +
    +        If a file with ``name`` already exists, an underscore plus a random 7
    +        character alphanumeric string is appended to the filename before the
    +        extension.
    +
    +        Previously, an underscore followed by a number (e.g. ``"_1"``, ``"_2"``,
    +        etc.) was appended to the filename until an avaible name in the
    +        destination directory was found. A malicious user could exploit this
    +        deterministic algorithm to create a denial-of-service attack. This
    +        change was also made in Django 1.5.9 and 1.4.14.
     
         .. method:: get_valid_name(name)
     
    
  • docs/releases/1.4.14.txt+20 0 modified
    @@ -18,3 +18,23 @@ To remedy this, URL reversing now ensures that no URL starts with two slashes
     (//), replacing the second slash with its URL encoded counterpart (%2F). This
     approach ensures that semantics stay the same, while making the URL relative to
     the domain and not to the scheme.
    +
    +File upload denial-of-service
    +=============================
    +
    +Before this release, Django's file upload handing in its default configuration
    +may degrade to producing a huge number of ``os.stat()`` system calls when a
    +duplicate filename is uploaded. Since ``stat()`` may invoke IO, this may produce
    +a huge data-dependent slowdown that slowly worsens over time. The net result is
    +that given enough time, a user with the ability to upload files can cause poor
    +performance in the upload handler, eventually causing it to become very slow
    +simply by uploading 0-byte files. At this point, even a slow network connection
    +and few HTTP requests would be all that is necessary to make a site unavailable.
    +
    +We've remedied the issue by changing the algorithm for generating file names
    +if a file with the uploaded name already exists.
    +:meth:`Storage.get_available_name()
    +<django.core.files.storage.Storage.get_available_name>` now appends an
    +underscore plus a random 7 character alphanumeric string (e.g. ``"_x3a1gho"``),
    +rather than iterating through an underscore followed by a number (e.g. ``"_1"``,
    +``"_2"``, etc.).
    
  • docs/releases/1.5.9.txt+20 0 modified
    @@ -18,3 +18,23 @@ To remedy this, URL reversing now ensures that no URL starts with two slashes
     (//), replacing the second slash with its URL encoded counterpart (%2F). This
     approach ensures that semantics stay the same, while making the URL relative to
     the domain and not to the scheme.
    +
    +File upload denial-of-service
    +=============================
    +
    +Before this release, Django's file upload handing in its default configuration
    +may degrade to producing a huge number of ``os.stat()`` system calls when a
    +duplicate filename is uploaded. Since ``stat()`` may invoke IO, this may produce
    +a huge data-dependent slowdown that slowly worsens over time. The net result is
    +that given enough time, a user with the ability to upload files can cause poor
    +performance in the upload handler, eventually causing it to become very slow
    +simply by uploading 0-byte files. At this point, even a slow network connection
    +and few HTTP requests would be all that is necessary to make a site unavailable.
    +
    +We've remedied the issue by changing the algorithm for generating file names
    +if a file with the uploaded name already exists.
    +:meth:`Storage.get_available_name()
    +<django.core.files.storage.Storage.get_available_name>` now appends an
    +underscore plus a random 7 character alphanumeric string (e.g. ``"_x3a1gho"``),
    +rather than iterating through an underscore followed by a number (e.g. ``"_1"``,
    +``"_2"``, etc.).
    
  • docs/releases/1.6.6.txt+20 0 modified
    @@ -19,6 +19,26 @@ To remedy this, URL reversing now ensures that no URL starts with two slashes
     approach ensures that semantics stay the same, while making the URL relative to
     the domain and not to the scheme.
     
    +File upload denial-of-service
    +=============================
    +
    +Before this release, Django's file upload handing in its default configuration
    +may degrade to producing a huge number of ``os.stat()`` system calls when a
    +duplicate filename is uploaded. Since ``stat()`` may invoke IO, this may produce
    +a huge data-dependent slowdown that slowly worsens over time. The net result is
    +that given enough time, a user with the ability to upload files can cause poor
    +performance in the upload handler, eventually causing it to become very slow
    +simply by uploading 0-byte files. At this point, even a slow network connection
    +and few HTTP requests would be all that is necessary to make a site unavailable.
    +
    +We've remedied the issue by changing the algorithm for generating file names
    +if a file with the uploaded name already exists.
    +:meth:`Storage.get_available_name()
    +<django.core.files.storage.Storage.get_available_name>` now appends an
    +underscore plus a random 7 character alphanumeric string (e.g. ``"_x3a1gho"``),
    +rather than iterating through an underscore followed by a number (e.g. ``"_1"``,
    +``"_2"``, etc.).
    +
     Bugfixes
     ========
     
    
  • tests/files/tests.py+13 9 modified
    @@ -13,12 +13,15 @@
     from django.core.files.uploadedfile import SimpleUploadedFile
     from django.core.files.temp import NamedTemporaryFile
     from django.test import TestCase
    -from django.utils import unittest
    +from django.utils import six, unittest
     from django.utils.six import StringIO
     
     from .models import Storage, temp_storage, temp_storage_location
     
     
    +FILE_SUFFIX_REGEX = '[A-Za-z0-9]{7}'
    +
    +
     class FileStorageTests(TestCase):
         def tearDown(self):
             shutil.rmtree(temp_storage_location)
    @@ -64,27 +67,28 @@ def test_files(self):
             # Save another file with the same name.
             obj2 = Storage()
             obj2.normal.save("django_test.txt", ContentFile("more content"))
    -        self.assertEqual(obj2.normal.name, "tests/django_test_1.txt")
    +        obj2_name = obj2.normal.name
    +        six.assertRegex(self, obj2_name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
             self.assertEqual(obj2.normal.size, 12)
     
             # Push the objects into the cache to make sure they pickle properly
             cache.set("obj1", obj1)
             cache.set("obj2", obj2)
    -        self.assertEqual(cache.get("obj2").normal.name, "tests/django_test_1.txt")
    +        six.assertRegex(self, cache.get("obj2").normal.name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
     
             # Deleting an object does not delete the file it uses.
             obj2.delete()
             obj2.normal.save("django_test.txt", ContentFile("more content"))
    -        self.assertEqual(obj2.normal.name, "tests/django_test_2.txt")
    +        self.assertNotEqual(obj2_name, obj2.normal.name)
    +        six.assertRegex(self, obj2.normal.name, "tests/django_test_%s.txt" % FILE_SUFFIX_REGEX)
     
             # Multiple files with the same name get _N appended to them.
    -        objs = [Storage() for i in range(3)]
    +        objs = [Storage() for i in range(2)]
             for o in objs:
                 o.normal.save("multiple_files.txt", ContentFile("Same Content"))
    -        self.assertEqual(
    -            [o.normal.name for o in objs],
    -            ["tests/multiple_files.txt", "tests/multiple_files_1.txt", "tests/multiple_files_2.txt"]
    -        )
    +        names = [o.normal.name for o in objs]
    +        self.assertEqual(names[0], "tests/multiple_files.txt")
    +        six.assertRegex(self, names[1], "tests/multiple_files_%s.txt" % FILE_SUFFIX_REGEX)
             for o in objs:
                 o.delete()
     
    
  • tests/file_storage/tests.py+13 8 modified
    @@ -35,6 +35,9 @@
         Image = None
     
     
    +FILE_SUFFIX_REGEX = '[A-Za-z0-9]{7}'
    +
    +
     class GetStorageClassTests(SimpleTestCase):
     
         def test_get_filesystem_storage(self):
    @@ -430,10 +433,9 @@ def test_race_condition(self):
             self.thread.start()
             name = self.save_file('conflict')
             self.thread.join()
    -        self.assertTrue(self.storage.exists('conflict'))
    -        self.assertTrue(self.storage.exists('conflict_1'))
    -        self.storage.delete('conflict')
    -        self.storage.delete('conflict_1')
    +        files = sorted(os.listdir(self.storage_dir))
    +        self.assertEqual(files[0], 'conflict')
    +        six.assertRegex(self, files[1], 'conflict_%s' % FILE_SUFFIX_REGEX)
     
     @unittest.skipIf(sys.platform.startswith('win'), "Windows only partially supports umasks and chmod.")
     class FileStoragePermissions(unittest.TestCase):
    @@ -477,9 +479,10 @@ def test_directory_with_dot(self):
             self.storage.save('dotted.path/test', ContentFile("1"))
             self.storage.save('dotted.path/test', ContentFile("2"))
     
    +        files = sorted(os.listdir(os.path.join(self.storage_dir, 'dotted.path')))
             self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path')))
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test')))
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_1')))
    +        self.assertEqual(files[0], 'test')
    +        six.assertRegex(self, files[1], 'test_%s' % FILE_SUFFIX_REGEX)
     
         def test_first_character_dot(self):
             """
    @@ -489,8 +492,10 @@ def test_first_character_dot(self):
             self.storage.save('dotted.path/.test', ContentFile("1"))
             self.storage.save('dotted.path/.test', ContentFile("2"))
     
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test')))
    -        self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_1')))
    +        files = sorted(os.listdir(os.path.join(self.storage_dir, 'dotted.path')))
    +        self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path')))
    +        self.assertEqual(files[0], '.test')
    +        six.assertRegex(self, files[1], '.test_%s' % FILE_SUFFIX_REGEX)
     
     class DimensionClosingBug(unittest.TestCase):
         """
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

13

News mentions

0

No linked articles in our index yet.