~ubuntu-branches/ubuntu/lucid/python-django/lucid-security

« back to all changes in this revision

Viewing changes to debian/patches/CVE-2014-0481.patch

  • Committer: Package Import Robot
  • Author(s): Marc Deslauriers
  • Date: 2014-09-10 13:07:32 UTC
  • Revision ID: package-import@ubuntu.com-20140910130732-ggo4hojqf9z22axy
Tags: 1.1.1-2ubuntu1.13
* SECURITY UPDATE: incorrect url validation in core.urlresolvers.reverse
  - debian/patches/CVE-2014-0480.patch: prevent reverse() from generating
    URLs pointing to other hosts in django/core/urlresolvers.py, added
    tests to tests/regressiontests/urlpatterns_reverse/{tests,urls}.py.
  - CVE-2014-0480
* SECURITY UPDATE: denial of service via file upload handling
  - debian/patches/CVE-2014-0481.patch: remove O(n) algorithm in
    django/core/files/storage.py, updated docs in
    docs/howto/custom-file-storage.txt, added tests to
    tests/modeltests/files/models.py,
    tests/regressiontests/file_storage/tests.py, backport
    get_random_string() to django/utils/crypto.py.
  - CVE-2014-0481
* SECURITY UPDATE: web session hijack via REMOTE_USER header
  - debian/patches/CVE-2014-0482.patch: modified RemoteUserMiddleware to
    logout on REMOTE_USE change in django/contrib/auth/middleware.py,
    added test to django/contrib/auth/tests/remote_user.py.
  - CVE-2014-0482
* SECURITY UPDATE: data leak in contrib.admin via query string manipulation
  - debian/patches/CVE-2014-0483.patch: validate to_field in
    django/contrib/admin/{options,exceptions}.py,
    django/contrib/admin/views/main.py, added tests to
    tests/regressiontests/admin_views/tests.py.
  - debian/patches/CVE-2014-0483-bug23329.patch: regression fix in
    django/contrib/admin/options.py, added tests to
    tests/regressiontests/admin_views/{models,tests}.py.
  - debian/patches/CVE-2014-0483-bug23431.patch: regression fix in
    django/contrib/admin/options.py, added tests to
    tests/regressiontests/admin_views/{models,tests}.py.
  - CVE-2014-0483
* debian/patches/fix_invalid_link_ftbfs.patch: remove test causing FTBFS.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
Backport of:
 
2
 
 
3
From 30042d475bf084c6723c6217a21598d9247a9c41 Mon Sep 17 00:00:00 2001
 
4
From: Tim Graham <timograham@gmail.com>
 
5
Date: Fri, 8 Aug 2014 10:20:08 -0400
 
6
Subject: [PATCH] [1.4.x] Fixed #23157 -- Removed O(n) algorithm when uploading
 
7
 duplicate file names.
 
8
 
 
9
This is a security fix. Disclosure following shortly.
 
10
---
 
11
 django/core/files/storage.py                | 11 +++++------
 
12
 docs/howto/custom-file-storage.txt          | 12 ++++++++++--
 
13
 docs/ref/files/storage.txt                  | 16 +++++++++++++---
 
14
 docs/releases/1.4.14.txt                    | 20 ++++++++++++++++++++
 
15
 tests/modeltests/files/tests.py             | 21 +++++++++++++--------
 
16
 tests/regressiontests/file_storage/tests.py | 23 ++++++++++++++---------
 
17
 6 files changed, 75 insertions(+), 28 deletions(-)
 
18
 
 
19
Index: python-django-1.1.1/django/core/files/storage.py
 
20
===================================================================
 
21
--- python-django-1.1.1.orig/django/core/files/storage.py       2014-09-10 14:17:48.090869173 -0400
 
22
+++ python-django-1.1.1/django/core/files/storage.py    2014-09-10 14:17:48.086869173 -0400
 
23
@@ -6,6 +6,7 @@
 
24
 from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
 
25
 from django.core.files import locks, File
 
26
 from django.core.files.move import file_move_safe
 
27
+from django.utils.crypto import get_random_string
 
28
 from django.utils.encoding import force_unicode, smart_str
 
29
 from django.utils.functional import LazyObject
 
30
 from django.utils.importlib import import_module
 
31
@@ -65,13 +66,13 @@
 
32
         """
 
33
         dir_name, file_name = os.path.split(name)
 
34
         file_root, file_ext = os.path.splitext(file_name)
 
35
-        # If the filename already exists, keep adding an underscore (before the
 
36
-        # file extension, if one exists) to the filename until the generated
 
37
-        # filename doesn't exist.
 
38
+        # If the filename already exists, add an underscore and a random 7
 
39
+        # character alphanumeric string (before the file extension, if one
 
40
+        # exists) to the filename until the generated filename doesn't exist.
 
41
         while self.exists(name):
 
42
-            file_root += '_'
 
43
             # file_ext includes the dot.
 
44
-            name = os.path.join(dir_name, file_root + file_ext)
 
45
+            name = os.path.join(dir_name, "%s_%s%s" % (file_root, get_random_string(7), file_ext))
 
46
+
 
47
         return name
 
48
 
 
49
     def path(self, name):
 
50
Index: python-django-1.1.1/django/utils/crypto.py
 
51
===================================================================
 
52
--- /dev/null   1970-01-01 00:00:00.000000000 +0000
 
53
+++ python-django-1.1.1/django/utils/crypto.py  2014-09-10 14:17:48.086869173 -0400
 
54
@@ -0,0 +1,45 @@
 
55
+"""
 
56
+Django's standard crypto functions and utilities.
 
57
+"""
 
58
+import hashlib
 
59
+import time
 
60
+
 
61
+# Use the system PRNG if possible
 
62
+import random
 
63
+try:
 
64
+    random = random.SystemRandom()
 
65
+    using_sysrandom = True
 
66
+except NotImplementedError:
 
67
+    import warnings
 
68
+    warnings.warn('A secure pseudo-random number generator is not available '
 
69
+                  'on your system. Falling back to Mersenne Twister.')
 
70
+    using_sysrandom = False
 
71
+
 
72
+from django.conf import settings
 
73
+
 
74
+
 
75
+def get_random_string(length=12,
 
76
+                      allowed_chars='abcdefghijklmnopqrstuvwxyz'
 
77
+                                    'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
 
78
+    """
 
79
+    Returns a securely generated random string.
 
80
+
 
81
+    The default length of 12 with the a-z, A-Z, 0-9 character set returns
 
82
+    a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
 
83
+    """
 
84
+    if not using_sysrandom:
 
85
+        # This is ugly, and a hack, but it makes things better than
 
86
+        # the alternative of predictability. This re-seeds the PRNG
 
87
+        # using a value that is hard for an attacker to predict, every
 
88
+        # time a random string is required. This may change the
 
89
+        # properties of the chosen random sequence slightly, but this
 
90
+        # is better than absolute predictability.
 
91
+        random.seed(
 
92
+            hashlib.sha256(
 
93
+                ("%s%s%s" % (
 
94
+                    random.getstate(),
 
95
+                    time.time(),
 
96
+                    settings.SECRET_KEY)).encode('utf-8')
 
97
+                ).digest())
 
98
+    return ''.join([random.choice(allowed_chars) for i in range(length)])
 
99
+
 
100
Index: python-django-1.1.1/docs/howto/custom-file-storage.txt
 
101
===================================================================
 
102
--- python-django-1.1.1.orig/docs/howto/custom-file-storage.txt 2014-09-10 14:17:48.090869173 -0400
 
103
+++ python-django-1.1.1/docs/howto/custom-file-storage.txt      2014-09-10 14:17:48.086869173 -0400
 
104
@@ -88,5 +88,13 @@
 
105
 will have already cleaned to a filename valid for the storage system, according
 
106
 to the ``get_valid_name()`` method described above.
 
107
 
 
108
-The code provided on ``Storage`` simply appends underscores to the filename
 
109
-until it finds one that's available in the destination directory.
 
110
+.. versionchanged:: 1.4.14
 
111
+
 
112
+    If a file with ``name`` already exists, an underscore plus a random 7
 
113
+    character alphanumeric string is appended to the filename before the
 
114
+    extension.
 
115
+
 
116
+    Previously, an underscore followed by a number (e.g. ``"_1"``, ``"_2"``,
 
117
+    etc.) was appended to the filename until an avaible name in the destination
 
118
+    directory was found. A malicious user could exploit this deterministic
 
119
+    algorithm to create a denial-of-service attack.
 
120
Index: python-django-1.1.1/tests/regressiontests/file_storage/tests.py
 
121
===================================================================
 
122
--- python-django-1.1.1.orig/tests/regressiontests/file_storage/tests.py        2014-09-10 14:17:48.090869173 -0400
 
123
+++ python-django-1.1.1/tests/regressiontests/file_storage/tests.py     2014-09-10 14:17:48.086869173 -0400
 
124
@@ -1,5 +1,6 @@
 
125
 # -*- coding: utf-8 -*-
 
126
 import os
 
127
+import re
 
128
 import shutil
 
129
 import sys
 
130
 import tempfile
 
131
@@ -25,6 +26,8 @@
 
132
 except ImportError:
 
133
     Image = None
 
134
 
 
135
+FILE_SUFFIX_REGEX = '[A-Za-z0-9]{7}'
 
136
+
 
137
 class FileStorageTests(unittest.TestCase):
 
138
     storage_class = FileSystemStorage
 
139
     
 
140
@@ -114,6 +117,15 @@
 
141
     def tearDown(self):
 
142
         shutil.rmtree(self.storage_dir)
 
143
 
 
144
+    def assertRegexpMatches(self, text, expected_regexp, msg=None):
 
145
+        """Fail the test unless the text matches the regular expression."""
 
146
+        if isinstance(expected_regexp, basestring):
 
147
+            expected_regexp = re.compile(expected_regexp)
 
148
+        if not expected_regexp.search(text):
 
149
+            msg = msg or "Regexp didn't match"
 
150
+            msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern, text)
 
151
+            raise self.failureException(msg)
 
152
+
 
153
     def save_file(self, name):
 
154
         name = self.storage.save(name, SlowFile("Data"))
 
155
 
 
156
@@ -121,10 +133,9 @@
 
157
         self.thread.start()
 
158
         name = self.save_file('conflict')
 
159
         self.thread.join()
 
160
-        self.assert_(self.storage.exists('conflict'))
 
161
-        self.assert_(self.storage.exists('conflict_'))
 
162
-        self.storage.delete('conflict')
 
163
-        self.storage.delete('conflict_')
 
164
+        files = sorted(os.listdir(self.storage_dir))
 
165
+        self.assertEqual(files[0], 'conflict')
 
166
+        self.assertRegexpMatches(files[1], 'conflict_%s' % FILE_SUFFIX_REGEX)
 
167
 
 
168
 class FileStoragePermissions(TestCase):
 
169
     def setUp(self):
 
170
@@ -151,6 +162,15 @@
 
171
     def tearDown(self):
 
172
         shutil.rmtree(self.storage_dir)
 
173
 
 
174
+    def assertRegexpMatches(self, text, expected_regexp, msg=None):
 
175
+        """Fail the test unless the text matches the regular expression."""
 
176
+        if isinstance(expected_regexp, basestring):
 
177
+            expected_regexp = re.compile(expected_regexp)
 
178
+        if not expected_regexp.search(text):
 
179
+            msg = msg or "Regexp didn't match"
 
180
+            msg = '%s: %r not found in %r' % (msg, expected_regexp.pattern, text)
 
181
+            raise self.failureException(msg)
 
182
+
 
183
     def test_directory_with_dot(self):
 
184
         """Regression test for #9610.
 
185
 
 
186
@@ -161,9 +181,10 @@
 
187
         self.storage.save('dotted.path/test', ContentFile("1"))
 
188
         self.storage.save('dotted.path/test', ContentFile("2"))
 
189
 
 
190
+        files = sorted(os.listdir(os.path.join(self.storage_dir, 'dotted.path')))
 
191
         self.failIf(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path')))
 
192
-        self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test')))
 
193
-        self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_')))
 
194
+        self.assertEqual(files[0], 'test')
 
195
+        self.assertRegexpMatches(files[1], 'test_%s' % FILE_SUFFIX_REGEX)
 
196
 
 
197
     def test_first_character_dot(self):
 
198
         """
 
199
@@ -176,10 +197,12 @@
 
200
         self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test')))
 
201
         # Before 2.6, a leading dot was treated as an extension, and so
 
202
         # underscore gets added to beginning instead of end.
 
203
+        files = sorted(os.listdir(os.path.join(self.storage_dir, 'dotted.path')))
 
204
+        self.assertEqual(files[0], '.test')
 
205
         if sys.version_info < (2, 6):
 
206
-            self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/_.test')))
 
207
+            self.assertRegexpMatches(files[1], '_%s.test' % FILE_SUFFIX_REGEX)
 
208
         else:
 
209
-            self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_')))
 
210
+            self.assertRegexpMatches(files[1], '.test_%s' % FILE_SUFFIX_REGEX)
 
211
 
 
212
 if Image is not None:
 
213
     class DimensionClosingBug(TestCase):
 
214
Index: python-django-1.1.1/tests/modeltests/files/models.py
 
215
===================================================================
 
216
--- python-django-1.1.1.orig/tests/modeltests/files/models.py   2014-09-10 14:17:48.090869173 -0400
 
217
+++ python-django-1.1.1/tests/modeltests/files/models.py        2014-09-10 14:18:57.074869562 -0400
 
218
@@ -92,8 +92,8 @@
 
219
 
 
220
 >>> obj2 = Storage()
 
221
 >>> obj2.normal.save('django_test.txt', ContentFile('more content'))
 
222
->>> obj2.normal
 
223
-<FieldFile: tests/django_test_.txt>
 
224
+>>> str(obj2.normal)[:-11]
 
225
+'tests/django_test_'
 
226
 >>> obj2.normal.size
 
227
 12
 
228
 
 
229
@@ -101,16 +101,16 @@
 
230
 
 
231
 >>> cache.set('obj1', obj1)
 
232
 >>> cache.set('obj2', obj2)
 
233
->>> cache.get('obj2').normal
 
234
-<FieldFile: tests/django_test_.txt>
 
235
+>>> str(cache.get('obj2').normal)[:-11]
 
236
+'tests/django_test_'
 
237
 
 
238
 # Deleting an object deletes the file it uses, if there are no other objects
 
239
 # still using that file.
 
240
 
 
241
 >>> obj2.delete()
 
242
 >>> obj2.normal.save('django_test.txt', ContentFile('more content'))
 
243
->>> obj2.normal
 
244
-<FieldFile: tests/django_test_.txt>
 
245
+>>> str(obj2.normal)[:-11]
 
246
+'tests/django_test_'
 
247
 
 
248
 # Default values allow an object to access a single file.
 
249