[Python-modules-commits] [python-django] 01/02: releasing package python-django version 1.4.5-1+deb7u8

Luke Faraone lfaraone at moszumanska.debian.org
Wed Jan 28 20:40:39 UTC 2015


This is an automated email from the git hooks/post-receive script.

lfaraone pushed a commit to branch debian/wheezy-lfaraone
in repository python-django.

commit 25ea0f8f8aee523866f57a894cb969f53d2e35e7
Author: Luke Faraone <lfaraone at dropbox.com>
Date:   Tue Aug 19 22:56:31 2014 -0700

    releasing package python-django version 1.4.5-1+deb7u8
---
 debian/changelog                                   |  10 +
 .../patches/02_disable-sources-in-sphinxdoc.diff   |   2 +-
 .../06_use_debian_geoip_database_as_default.diff   |   8 +-
 debian/patches/admin-data-leak-1.4.diff            | 115 +++++
 debian/patches/cache-csrf-1.4.x.patch              |  14 +-
 debian/patches/drop_fix_ie_for_vary_1_4.diff       |  12 +-
 debian/patches/file-upload-1.4.diff                | 247 +++++++++++
 debian/patches/is_safe_url-1.4.diff                |  14 +-
 debian/patches/is_safe_url_1_4.diff                |  18 +-
 debian/patches/mysql-typecast-1.4.x.diff           |  30 +-
 debian/patches/remote-user-1.4.diff                |  88 ++++
 debian/patches/reverse-1.4.diff                    |  41 ++
 debian/patches/reverse-execution-1.4.x.patch       |  29 +-
 debian/patches/series                              |   4 +
 debian/patches/six-regex.patch                     | 490 +++++++++++++++++++++
 debian/patches/ssi-tag-1.4.diff                    |   8 +-
 16 files changed, 1039 insertions(+), 91 deletions(-)

diff --git a/debian/changelog b/debian/changelog
index a616732..38a8623 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,13 @@
+python-django (1.4.5-1+deb7u8) wheezy-security; urgency=high
+
+  * New upstream security release.
+    - reverse() can generate URLs pointing to other hosts (CVE-2014-0480)                                                                                                                                          
+    - file upload denial of service (CVE-2014-0481)                                                                                                                                                                
+    - RemoteUserMiddleware session hijacking (CVE-2014-0482)                                                                                                                                                       
+    - data leakage via querystring manipulation in admin (CVE-2014-0483)   
+
+ -- Luke Faraone <lfaraone at debian.org>  Wed, 20 Aug 2014 01:46:17 -0700
+
 python-django (1.4.5-1+deb7u7) stable-security; urgency=high
 
   * New upstream security release.
diff --git a/debian/patches/02_disable-sources-in-sphinxdoc.diff b/debian/patches/02_disable-sources-in-sphinxdoc.diff
index 9541dd7..5904095 100644
--- a/debian/patches/02_disable-sources-in-sphinxdoc.diff
+++ b/debian/patches/02_disable-sources-in-sphinxdoc.diff
@@ -9,7 +9,7 @@ Origin: vendor
 
 --- a/docs/conf.py
 +++ b/docs/conf.py
-@@ -168,7 +168,10 @@ html_additional_pages = {}
+@@ -168,7 +168,10 @@
  #html_split_index = False
  
  # If true, links to the reST sources are added to the pages.
diff --git a/debian/patches/06_use_debian_geoip_database_as_default.diff b/debian/patches/06_use_debian_geoip_database_as_default.diff
index 44503f1..570cc9f 100644
--- a/debian/patches/06_use_debian_geoip_database_as_default.diff
+++ b/debian/patches/06_use_debian_geoip_database_as_default.diff
@@ -9,7 +9,7 @@ Author: Tapio Rantala <tapio.rantala at iki.fi>
 
 --- a/django/contrib/gis/geoip/base.py
 +++ b/django/contrib/gis/geoip/base.py
-@@ -61,7 +61,8 @@ class GeoIP(object):
+@@ -61,7 +61,8 @@
          * path: Base directory to where GeoIP data is located or the full path
              to where the city or country data files (*.dat) are located.
              Assumes that both the city and country data sets are located in
@@ -19,7 +19,7 @@ Author: Tapio Rantala <tapio.rantala at iki.fi>
  
          * cache: The cache settings when opening up the GeoIP datasets,
              and may be an integer in (0, 1, 2, 4, 8) corresponding to
-@@ -70,11 +71,13 @@ class GeoIP(object):
+@@ -70,11 +71,13 @@
              settings,  respectively.  Defaults to 0, meaning that the data is read
              from the disk.
  
@@ -38,7 +38,7 @@ Author: Tapio Rantala <tapio.rantala at iki.fi>
          """
          # Checking the given cache option.
          if cache in self.cache_options:
-@@ -84,8 +87,7 @@ class GeoIP(object):
+@@ -84,8 +87,7 @@
  
          # Getting the GeoIP data path.
          if not path:
@@ -48,7 +48,7 @@ Author: Tapio Rantala <tapio.rantala at iki.fi>
          if not isinstance(path, basestring):
              raise TypeError('Invalid path type: %s' % type(path).__name__)
  
-@@ -98,7 +100,7 @@ class GeoIP(object):
+@@ -98,7 +100,7 @@
                  self._country = GeoIP_open(country_db, cache)
                  self._country_file = country_db
  
diff --git a/debian/patches/admin-data-leak-1.4.diff b/debian/patches/admin-data-leak-1.4.diff
new file mode 100644
index 0000000..7e9b714
--- /dev/null
+++ b/debian/patches/admin-data-leak-1.4.diff
@@ -0,0 +1,115 @@
+commit 027bd348642007617518379f8b02546abacaa6e0
+Author: Simon Charette <charette.s at gmail.com>
+Date:   Mon Aug 11 15:36:16 2014 -0400
+
+    [1.4.x] Prevented data leakage in contrib.admin via query string manipulation.
+    
+    This is a security fix. Disclosure following shortly.
+
+--- /dev/null
++++ b/django/contrib/admin/exceptions.py
+@@ -0,0 +1,6 @@
++from django.core.exceptions import SuspiciousOperation
++
++
++class DisallowedModelAdminToField(SuspiciousOperation):
++    """Invalid to_field was passed to admin view via URL query string"""
++    pass
+--- a/django/contrib/admin/options.py
++++ b/django/contrib/admin/options.py
+@@ -269,6 +269,24 @@
+         clean_lookup = LOOKUP_SEP.join(parts)
+         return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
+ 
++    def to_field_allowed(self, request, to_field):
++        opts = self.model._meta
++
++        try:
++            field = opts.get_field(to_field)
++        except FieldDoesNotExist:
++            return False
++
++        # Make sure at least one of the models registered for this site
++        # references this field.
++        registered_models = self.admin_site._registry
++        for related_object in opts.get_all_related_objects():
++            if (related_object.model in registered_models and
++                    field == related_object.field.rel.get_related_field()):
++                return True
++
++        return False
++
+     def has_add_permission(self, request):
+         """
+         Returns True if the given request has permission to add an object.
+--- a/django/contrib/admin/views/main.py
++++ b/django/contrib/admin/views/main.py
+@@ -10,6 +10,7 @@
+ from django.utils.http import urlencode
+ 
+ from django.contrib.admin import FieldListFilter
++from django.contrib.admin.exceptions import DisallowedModelAdminToField
+ from django.contrib.admin.options import IncorrectLookupParameters
+ from django.contrib.admin.util import (quote, get_fields_from_path,
+     lookup_needs_distinct, prepare_lookup_value)
+@@ -56,7 +57,10 @@
+             self.page_num = 0
+         self.show_all = ALL_VAR in request.GET
+         self.is_popup = IS_POPUP_VAR in request.GET
+-        self.to_field = request.GET.get(TO_FIELD_VAR)
++        to_field = request.GET.get(TO_FIELD_VAR)
++        if to_field and not model_admin.to_field_allowed(request, to_field):
++            raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
++        self.to_field = to_field
+         self.params = dict(request.GET.items())
+         if PAGE_VAR in self.params:
+             del self.params[PAGE_VAR]
+--- a/tests/regressiontests/admin_views/tests.py
++++ b/tests/regressiontests/admin_views/tests.py
+@@ -13,11 +13,12 @@
+ from django.core.urlresolvers import reverse
+ # Register auth models with the admin.
+ from django.contrib import admin
++from django.contrib.admin.exceptions import DisallowedModelAdminToField
+ from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
+ from django.contrib.admin.models import LogEntry, DELETION
+ from django.contrib.admin.sites import LOGIN_FORM_KEY
+ from django.contrib.admin.util import quote
+-from django.contrib.admin.views.main import IS_POPUP_VAR
++from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
+ from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
+ from django.contrib.auth import REDIRECT_FIELD_NAME
+ from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD
+@@ -572,6 +573,19 @@
+         response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk)
+         self.assertEqual(response.status_code, 200)
+ 
++    def test_disallowed_to_field(self):
++        with self.assertRaises(DisallowedModelAdminToField):
++            response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'missing_field'})
++
++        # Specifying a field that is not refered by any other model registered
++        # to this admin site should raise an exception.
++        with self.assertRaises(DisallowedModelAdminToField):
++            response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'name'})
++
++        # Specifying a field referenced by another model should be allowed.
++        response = self.client.get("/test_admin/admin/admin_views/section/", {TO_FIELD_VAR: 'id'})
++        self.assertEqual(response.status_code, 200)
++
+     def test_allowed_filtering_15103(self):
+         """
+         Regressions test for ticket 15103 - filtering on fields defined in a
+@@ -2061,10 +2075,9 @@
+         """Ensure that the to_field GET parameter is preserved when a search
+         is performed. Refs #10918.
+         """
+-        from django.contrib.admin.views.main import TO_FIELD_VAR
+-        response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR)
++        response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=id' % TO_FIELD_VAR)
+         self.assertContains(response, "\n1 user\n")
+-        self.assertContains(response, '<input type="hidden" name="t" value="username"/>', html=True)
++        self.assertContains(response, '<input type="hidden" name="%s" value="id"/>' % TO_FIELD_VAR, html=True)
+ 
+     def test_exact_matches(self):
+         response = self.client.get('/test_admin/admin/admin_views/recommendation/?q=bar')
diff --git a/debian/patches/cache-csrf-1.4.x.patch b/debian/patches/cache-csrf-1.4.x.patch
index 5261baa..92c93bd 100644
--- a/debian/patches/cache-csrf-1.4.x.patch
+++ b/debian/patches/cache-csrf-1.4.x.patch
@@ -29,11 +29,9 @@ responses. The heuristic for this will be:
 3. The ``Vary: Cookie`` header is set on the response, then the
    response will not be cached.
 
-diff --git a/django/middleware/cache.py b/django/middleware/cache.py
-index 34bf0ca..760ba4e 100644
 --- a/django/middleware/cache.py
 +++ b/django/middleware/cache.py
-@@ -50,7 +50,8 @@ More details about how the caching works:
+@@ -50,7 +50,8 @@
  
  from django.conf import settings
  from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS
@@ -43,7 +41,7 @@ index 34bf0ca..760ba4e 100644
  
  
  class UpdateCacheMiddleware(object):
-@@ -93,8 +94,15 @@ class UpdateCacheMiddleware(object):
+@@ -93,8 +94,15 @@
          if not self._should_update_cache(request, response):
              # We don't need to update the cache, just return.
              return response
@@ -59,11 +57,9 @@ index 34bf0ca..760ba4e 100644
          # Try to get the timeout from the "max-age" section of the "Cache-
          # Control" header before reverting to using the default cache_timeout
          # length.
-diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
-index bd29cde..b0be259 100644
 --- a/tests/regressiontests/cache/tests.py
 +++ b/tests/regressiontests/cache/tests.py
-@@ -17,10 +17,12 @@ from django.core import management
+@@ -17,10 +17,12 @@
  from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS
  from django.core.cache.backends.base import (CacheKeyWarning,
      InvalidCacheBackendError)
@@ -76,7 +72,7 @@ index bd29cde..b0be259 100644
  from django.template import Template
  from django.template.response import TemplateResponse
  from django.test import TestCase, TransactionTestCase, RequestFactory
-@@ -1418,6 +1420,10 @@ def hello_world_view(request, value):
+@@ -1418,6 +1420,10 @@
      return HttpResponse('Hello World %s' % value)
  
  
@@ -87,7 +83,7 @@ index bd29cde..b0be259 100644
  class CacheMiddlewareTest(TestCase):
  
      def setUp(self):
-@@ -1635,6 +1641,27 @@ class CacheMiddlewareTest(TestCase):
+@@ -1635,6 +1641,27 @@
          response = other_with_timeout_view(request, '18')
          self.assertEqual(response.content, 'Hello World 18')
  
diff --git a/debian/patches/drop_fix_ie_for_vary_1_4.diff b/debian/patches/drop_fix_ie_for_vary_1_4.diff
index 460b7f4..0ff5985 100644
--- a/debian/patches/drop_fix_ie_for_vary_1_4.diff
+++ b/debian/patches/drop_fix_ie_for_vary_1_4.diff
@@ -22,11 +22,9 @@ longer stripped from the response. In addition, modifications to the
 ``Content-Disposition`` header, have also been removed as they were
 found to have similar issues.
 
-diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
-index a0918bf..99f81a6 100644
 --- a/django/core/handlers/base.py
 +++ b/django/core/handlers/base.py
-@@ -14,8 +14,6 @@ class BaseHandler(object):
+@@ -14,8 +14,6 @@
      response_fixes = [
          http.fix_location_header,
          http.conditional_content_removal,
@@ -35,11 +33,9 @@ index a0918bf..99f81a6 100644
      ]
  
      def __init__(self):
-diff --git a/django/http/utils.py b/django/http/utils.py
-index 0180864..f98ca93 100644
 --- a/django/http/utils.py
 +++ b/django/http/utils.py
-@@ -31,57 +31,3 @@ def conditional_content_removal(request, response):
+@@ -31,57 +31,3 @@
      if request.method == 'HEAD':
          response.content = ''
      return response
@@ -97,11 +93,9 @@ index 0180864..f98ca93 100644
 -
 -    return response
 -
-diff --git a/tests/regressiontests/utils/http.py b/tests/regressiontests/utils/http.py
-index 16c7daa..9e05a94 100644
 --- a/tests/regressiontests/utils/http.py
 +++ b/tests/regressiontests/utils/http.py
-@@ -56,50 +56,6 @@ class TestUtilsHttp(unittest.TestCase):
+@@ -56,50 +56,6 @@
          ]
          self.assertTrue(result in acceptable_results)
  
diff --git a/debian/patches/file-upload-1.4.diff b/debian/patches/file-upload-1.4.diff
new file mode 100644
index 0000000..57eeb83
--- /dev/null
+++ b/debian/patches/file-upload-1.4.diff
@@ -0,0 +1,247 @@
+commit 30042d475bf084c6723c6217a21598d9247a9c41
+Author: Tim Graham <timograham at gmail.com>
+Date:   Fri Aug 8 10:20:08 2014 -0400
+
+    [1.4.x] Fixed #23157 -- Removed O(n) algorithm when uploading duplicate file names.
+    
+    This is a security fix. Disclosure following shortly.
+
+Debian note: This patch also includes the minimum backport of additional
+functionality to django.utils.six to support this patch, but does not change
+any functionality therein.
+
+--- a/django/core/files/storage.py
++++ b/django/core/files/storage.py
+@@ -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 @@
+         """
+         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
+ 
+--- a/docs/howto/custom-file-storage.txt
++++ b/docs/howto/custom-file-storage.txt
+@@ -86,5 +86,13 @@
+ 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.
+--- a/docs/ref/files/storage.txt
++++ b/docs/ref/files/storage.txt
+@@ -18,7 +18,7 @@
+ .. 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 @@
+     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 @@
+         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)
+ 
+--- a/tests/modeltests/files/tests.py
++++ b/tests/modeltests/files/tests.py
+@@ -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 @@
+         # 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()
+ 
+--- a/tests/regressiontests/file_storage/tests.py
++++ b/tests/regressiontests/file_storage/tests.py
+@@ -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 @@
+         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 @@
+         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 @@
+         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):
+     """
+--- a/django/utils/six.py
++++ b/django/utils/six.py
+@@ -370,13 +370,22 @@
+ 
+ if PY3:
+     _iterlists = "lists"
++    _assertRaisesRegex = "assertRaisesRegex"
++    _assertRegex = "assertRegex"
+ else:
++    _assertRaisesRegex = "assertRaisesRegexp"
+     _iterlists = "iterlists"
++    _assertRegex = "assertRegexpMatches"
+ 
+ def iterlists(d):
+     """Return an iterator over the values of a MultiValueDict."""
+     return getattr(d, _iterlists)()
+ 
++def assertRaisesRegex(self, *args, **kwargs):
++    return getattr(self, _assertRaisesRegex)(*args, **kwargs)
++
++def assertRegex(self, *args, **kwargs):
++    return getattr(self, _assertRegex)(*args, **kwargs)
+ 
+ add_move(MovedModule("_dummy_thread", "dummy_thread"))
+ add_move(MovedModule("_thread", "thread"))
diff --git a/debian/patches/is_safe_url-1.4.diff b/debian/patches/is_safe_url-1.4.diff
index 757cac0..e55478c 100644
--- a/debian/patches/is_safe_url-1.4.diff
+++ b/debian/patches/is_safe_url-1.4.diff
@@ -26,11 +26,9 @@ To remedy this issue, the ``is_safe_url()`` function will be modified
 to properly recognize and reject URLs which specify a scheme other
 than HTTP or HTTPS.
 
-diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py
-index 603d380..6d7029b 100644
 --- a/django/contrib/auth/tests/views.py
 +++ b/django/contrib/auth/tests/views.py
-@@ -309,7 +309,8 @@ class LoginTest(AuthViewsTestCase):
+@@ -309,7 +309,8 @@
          for bad_url in ('http://example.com',
                          'https://example.com',
                          'ftp://exampel.com',
@@ -40,7 +38,7 @@ index 603d380..6d7029b 100644
  
              nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
                  'url': login_url,
-@@ -330,6 +331,7 @@ class LoginTest(AuthViewsTestCase):
+@@ -330,6 +331,7 @@
                           '/view?param=ftp://exampel.com',
                           'view/?param=//example.com',
                           'https:///',
@@ -48,7 +46,7 @@ index 603d380..6d7029b 100644
                           '//testserver/',
                           '/url%20with%20spaces/'):  # see ticket #12534
              safe_url = '%(url)s?%(next)s=%(good_url)s' % {
-@@ -467,7 +469,8 @@ class LogoutTest(AuthViewsTestCase):
+@@ -467,7 +469,8 @@
          for bad_url in ('http://example.com',
                          'https://example.com',
                          'ftp://exampel.com',
@@ -58,7 +56,7 @@ index 603d380..6d7029b 100644
              nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
                  'url': logout_url,
                  'next': REDIRECT_FIELD_NAME,
-@@ -486,6 +489,7 @@ class LogoutTest(AuthViewsTestCase):
+@@ -486,6 +489,7 @@
                           '/view?param=ftp://exampel.com',
                           'view/?param=//example.com',
                           'https:///',
@@ -66,11 +64,9 @@ index 603d380..6d7029b 100644
                           '//testserver/',
                           '/url%20with%20spaces/'):  # see ticket #12534
              safe_url = '%(url)s?%(next)s=%(good_url)s' % {
-diff --git a/django/utils/http.py b/django/utils/http.py
-index d2e4eb5..21c84dc 100644
 --- a/django/utils/http.py
 +++ b/django/utils/http.py
-@@ -228,11 +228,12 @@ else:
+@@ -228,11 +228,12 @@
  def is_safe_url(url, host=None):
      """
      Return ``True`` if the url is a safe redirection (i.e. it doesn't point to
diff --git a/debian/patches/is_safe_url_1_4.diff b/debian/patches/is_safe_url_1_4.diff
index c6af21d..7c450c1 100644
--- a/debian/patches/is_safe_url_1_4.diff
+++ b/debian/patches/is_safe_url_1_4.diff
@@ -17,11 +17,9 @@ accepted by some browsers with more liberal URL parsing.
 To remedy this, the validation in ``is_safe_url()`` has been tightened
 to be able to handle and correctly validate these malformed URLs.
 
-diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py
-index 6d7029b..2b72cd4 100644
 --- a/django/contrib/auth/tests/views.py
 +++ b/django/contrib/auth/tests/views.py
-@@ -307,8 +307,10 @@ class LoginTest(AuthViewsTestCase):
+@@ -307,8 +307,10 @@
  
          # Those URLs should not pass the security check
          for bad_url in ('http://example.com',
@@ -32,7 +30,7 @@ index 6d7029b..2b72cd4 100644
                          '//example.com',
                          'javascript:alert("XSS")'):
  
-@@ -330,8 +332,8 @@ class LoginTest(AuthViewsTestCase):
+@@ -330,8 +332,8 @@
                           '/view/?param=https://example.com',
                           '/view?param=ftp://exampel.com',
                           'view/?param=//example.com',
@@ -43,7 +41,7 @@ index 6d7029b..2b72cd4 100644
                           '//testserver/',
                           '/url%20with%20spaces/'):  # see ticket #12534
              safe_url = '%(url)s?%(next)s=%(good_url)s' % {
-@@ -467,8 +469,10 @@ class LogoutTest(AuthViewsTestCase):
+@@ -467,8 +469,10 @@
  
          # Those URLs should not pass the security check
          for bad_url in ('http://example.com',
@@ -54,7 +52,7 @@ index 6d7029b..2b72cd4 100644
                          '//example.com',
                          'javascript:alert("XSS")'):
              nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
-@@ -488,8 +492,8 @@ class LogoutTest(AuthViewsTestCase):
+@@ -488,8 +492,8 @@
                           '/view/?param=https://example.com',
                           '/view?param=ftp://exampel.com',
                           'view/?param=//example.com',
@@ -65,11 +63,9 @@ index 6d7029b..2b72cd4 100644
                           '//testserver/',
                           '/url%20with%20spaces/'):  # see ticket #12534
              safe_url = '%(url)s?%(next)s=%(good_url)s' % {
-diff --git a/django/utils/http.py b/django/utils/http.py
-index 21c84dc..2d40489 100644
 --- a/django/utils/http.py
 +++ b/django/utils/http.py
-@@ -234,6 +234,18 @@ def is_safe_url(url, host=None):
+@@ -234,6 +234,18 @@
      """
      if not url:
          return False
@@ -88,11 +84,9 @@ index 21c84dc..2d40489 100644
 +        return False
      return (not url_info[1] or url_info[1] == host) and \
          (not url_info[0] or url_info[0] in ['http', 'https'])
-diff --git a/tests/regressiontests/utils/http.py b/tests/regressiontests/utils/http.py
-index 16c7daa..c50524e 100644
 --- a/tests/regressiontests/utils/http.py
 +++ b/tests/regressiontests/utils/http.py
-@@ -122,3 +122,33 @@ class TestUtilsHttp(unittest.TestCase):
+@@ -122,3 +122,33 @@
          for n, b36 in [(0, '0'), (1, '1'), (42, '16'), (818469960, 'django')]:
              self.assertEqual(http.int_to_base36(n), b36)
              self.assertEqual(http.base36_to_int(b36), n)
diff --git a/debian/patches/mysql-typecast-1.4.x.diff b/debian/patches/mysql-typecast-1.4.x.diff
index 7fc339a..6a97fd0 100644
--- a/debian/patches/mysql-typecast-1.4.x.diff
+++ b/debian/patches/mysql-typecast-1.4.x.diff
@@ -29,11 +29,9 @@ appropriate type conversions, and users of the ``raw()`` and
 SQL or SQL fragments -- will be advised to ensure they perform
 appropriate manual type conversions prior to executing queries.
 
-diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
-index 527a3c0..690a671 100644
 --- a/django/db/models/fields/__init__.py
 +++ b/django/db/models/fields/__init__.py
-@@ -911,6 +911,12 @@ class FilePathField(Field):
+@@ -911,6 +911,12 @@
          kwargs['max_length'] = kwargs.get('max_length', 100)
          Field.__init__(self, verbose_name, name, **kwargs)
  
@@ -46,7 +44,7 @@ index 527a3c0..690a671 100644
      def formfield(self, **kwargs):
          defaults = {
              'path': self.path,
-@@ -1010,6 +1016,12 @@ class IPAddressField(Field):
+@@ -1010,6 +1016,12 @@
          kwargs['max_length'] = 15
          Field.__init__(self, *args, **kwargs)
  
@@ -59,7 +57,7 @@ index 527a3c0..690a671 100644
      def get_internal_type(self):
          return "IPAddressField"
  
-@@ -1047,12 +1059,14 @@ class GenericIPAddressField(Field):
+@@ -1047,12 +1059,14 @@
          return value or None
  
      def get_prep_value(self, value):
@@ -75,11 +73,9 @@ index 527a3c0..690a671 100644
  
      def formfield(self, **kwargs):
          defaults = {'form_class': forms.GenericIPAddressField}
-diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt
-index daaede8..fcbda03 100644
 --- a/docs/howto/custom-model-fields.txt
 +++ b/docs/howto/custom-model-fields.txt
-@@ -482,6 +482,16 @@ For example::
+@@ -482,6 +482,16 @@
              return ''.join([''.join(l) for l in (value.north,
                      value.east, value.south, value.west)])
  
@@ -96,11 +92,9 @@ index daaede8..fcbda03 100644
  Converting query values to database values
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  
-diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt
-index 4c18658..2691979 100644
 --- a/docs/ref/databases.txt
 +++ b/docs/ref/databases.txt
-@@ -432,6 +432,22 @@ MySQL does not support the ``NOWAIT`` option to the ``SELECT ... FOR UPDATE``
+@@ -432,6 +432,22 @@
  statement. If ``select_for_update()`` is used with ``nowait=True`` then a
  ``DatabaseError`` will be raised.
  
@@ -123,11 +117,9 @@ index 4c18658..2691979 100644
  .. _sqlite-notes:
  
  SQLite notes
-diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
-index 022a251..2decddb 100644
 --- a/docs/ref/models/querysets.txt
 +++ b/docs/ref/models/querysets.txt
-@@ -1041,6 +1041,16 @@ of the arguments is required, but you should use at least one of them.
+@@ -1041,6 +1041,16 @@
  
        Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
  
@@ -144,11 +136,9 @@ index 022a251..2decddb 100644
  defer
  ~~~~~
  
-diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt
-index 80038e5..d387ad5 100644
 --- a/docs/topics/db/sql.txt
 +++ b/docs/topics/db/sql.txt
-@@ -69,6 +69,16 @@ options that make it very powerful.
+@@ -69,6 +69,16 @@
      database, but does nothing to enforce that. If the query does not
      return rows, a (possibly cryptic) error will result.
  
@@ -165,11 +155,9 @@ index 80038e5..d387ad5 100644
  Mapping query fields to model fields
  ------------------------------------
  
-diff --git a/tests/regressiontests/model_fields/tests.py b/tests/regressiontests/model_fields/tests.py
-index a71ea77..30f0e7d 100644
 --- a/tests/regressiontests/model_fields/tests.py
 +++ b/tests/regressiontests/model_fields/tests.py
-@@ -6,8 +6,15 @@ from decimal import Decimal
+@@ -6,8 +6,15 @@
  from django import test
  from django import forms
  from django.core.exceptions import ValidationError
@@ -186,7 +174,7 @@ index a71ea77..30f0e7d 100644
  from django.utils import unittest
  
  from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post,
-@@ -373,3 +380,88 @@ class FileFieldTests(unittest.TestCase):
+@@ -373,3 +380,88 @@
          field = d._meta.get_field('myfile')
          field.save_form_data(d, 'else.txt')
          self.assertEqual(d.myfile, 'else.txt')
diff --git a/debian/patches/remote-user-1.4.diff b/debian/patches/remote-user-1.4.diff
new file mode 100644
index 0000000..d12fa6e
--- /dev/null
+++ b/debian/patches/remote-user-1.4.diff
@@ -0,0 +1,88 @@
+commit c9e3b9949cd55f090591fbdc4a114fcb8368b6d9
+Author: Preston Holmes <preston at ptone.com>
+Date:   Mon Aug 11 12:04:53 2014 -0400
+
+    [1.4.x] Fixed #23066 -- Modified RemoteUserMiddleware to logout on REMOTE_USE change.
+    
+    This is a security fix. Disclosure following shortly.
+
+--- a/django/contrib/auth/middleware.py
++++ b/django/contrib/auth/middleware.py
+@@ -1,4 +1,5 @@
+ from django.contrib import auth
++from django.contrib.auth.backends import RemoteUserBackend
+ from django.core.exceptions import ImproperlyConfigured
+ from django.utils.functional import SimpleLazyObject
+ 
+@@ -47,9 +48,11 @@
+         try:
+             username = request.META[self.header]
+         except KeyError:
+-            # If specified header doesn't exist then return (leaving
+-            # request.user set to AnonymousUser by the
+-            # AuthenticationMiddleware).
++            # If specified header doesn't exist then remove any existing
++            # authenticated remote-user, or return (leaving request.user set to
++            # AnonymousUser by the AuthenticationMiddleware).
++            if request.user.is_authenticated():
++                self._remove_invalid_user(request)
+             return
+         # If the user is already authenticated and that user is the user we are
+         # getting passed in the headers, then the correct user is already
+@@ -57,6 +60,11 @@
+         if request.user.is_authenticated():
+             if request.user.username == self.clean_username(username, request):
+                 return
++            else:
++                # An authenticated user is associated with the request, but
++                # it does not match the authorized user in the header.
++                self._remove_invalid_user(request)
++
+         # We are seeing this user for the first time in this session, attempt
+         # to authenticate the user.
+         user = auth.authenticate(remote_user=username)
+@@ -78,3 +86,17 @@
+         except AttributeError: # Backend has no clean_username method.
+             pass
+         return username
++
++    def _remove_invalid_user(self, request):
++        """
++        Removes the current authenticated user in the request which is invalid
++        but only if the user is authenticated via the RemoteUserBackend.
++        """
++        try:
++            stored_backend = auth.load_backend(request.session.get(auth.BACKEND_SESSION_KEY, ''))
++        except ImproperlyConfigured:
++            # backend failed to load
++            auth.logout(request)
++        else:
++            if isinstance(stored_backend, RemoteUserBackend):
++                auth.logout(request)
+--- a/django/contrib/auth/tests/remote_user.py
++++ b/django/contrib/auth/tests/remote_user.py
+@@ -95,6 +95,24 @@
+         response = self.client.get('/remote_user/', REMOTE_USER=self.known_user)
+         self.assertEqual(default_login, response.context['user'].last_login)
+ 
++    def test_user_switch_forces_new_login(self):
++        """
++        Tests that if the username in the header changes between requests
++        that the original user is logged out
++        """
++        User.objects.create(username='knownuser')
++        # Known user authenticates
++        response = self.client.get('/remote_user/',
++                                   **{'REMOTE_USER': self.known_user})
++        self.assertEqual(response.context['user'].username, 'knownuser')
++        # During the session, the REMOTE_USER changes to a different user.
++        response = self.client.get('/remote_user/',
++                                   **{'REMOTE_USER': "newnewuser"})
++        # Ensure that the current user is not the prior remote_user
++        # In backends that create a new user, username is "newnewuser"
++        # In backends that do not create new users, it is '' (anonymous user)
++        self.assertNotEqual(response.context['user'].username, 'knownuser')
++
+     def tearDown(self):
+         """Restores settings to avoid breaking other tests."""
+         settings.MIDDLEWARE_CLASSES = self.curr_middleware
diff --git a/debian/patches/reverse-1.4.diff b/debian/patches/reverse-1.4.diff
new file mode 100644
index 0000000..7e0763f
--- /dev/null
+++ b/debian/patches/reverse-1.4.diff
@@ -0,0 +1,41 @@
+commit c2fe73133b62a1d9e8f7a6b43966570b14618d7e
+Author: Florian Apolloner <florian at apolloner.eu>
+Date:   Thu Jul 17 21:59:28 2014 +0200
+
+    [1.4.x] Prevented reverse() from generating URLs pointing to other hosts.
+    
+    This is a security fix. Disclosure following shortly.
+
+--- a/django/core/urlresolvers.py
++++ b/django/core/urlresolvers.py
+@@ -406,6 +406,8 @@
+                     unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()])
+                     candidate = (prefix_norm + result) % unicode_kwargs
+                 if re.search(u'^%s%s' % (_prefix, pattern), candidate, re.UNICODE):
++                    if candidate.startswith('//'):
++                        candidate = '/%%2F%s' % candidate[2:]
+                     return candidate
+         # lookup_view can be URL label, or dotted path, or callable, Any of
+         # these can be passed in at the top, but callables are not friendly in
+--- a/tests/regressiontests/urlpatterns_reverse/tests.py
++++ b/tests/regressiontests/urlpatterns_reverse/tests.py
+@@ -142,6 +142,9 @@
+     ('defaults', '/defaults_view2/3/', [], {'arg1': 3, 'arg2': 2}),
+     ('defaults', NoReverseMatch, [], {'arg1': 3, 'arg2': 3}),
+     ('defaults', NoReverseMatch, [], {'arg2': 1}),
++
++    # Security tests
++    ('security', '/%2Fexample.com/security/', ['/example.com'], {}),
+ )
+ 
+ class NoURLPatternsTests(TestCase):
+--- a/tests/regressiontests/urlpatterns_reverse/urls.py
++++ b/tests/regressiontests/urlpatterns_reverse/urls.py
+@@ -71,4 +71,7 @@
+     (r'defaults_view2/(?P<arg1>\d+)/', 'defaults_view', {'arg2': 2}, 'defaults'),
+ 
+     url('^includes/', include(other_patterns)),
++
++    # Security tests
++    url('(.+)/security/$', empty_view, name='security'),
+ )
diff --git a/debian/patches/reverse-execution-1.4.x.patch b/debian/patches/reverse-execution-1.4.x.patch
index da6e839..63e6418 100644
--- a/debian/patches/reverse-execution-1.4.x.patch
+++ b/debian/patches/reverse-execution-1.4.x.patch
@@ -38,11 +38,9 @@ paths based on the view-containing modules listed in the project's URL
 pattern configuration, so as to ensure that only modules the developer
 intended to be imported in this fashion can or will be imported.
 
-diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
-index 1497d43..6a2aec7 100644
 --- a/django/core/urlresolvers.py
 +++ b/django/core/urlresolvers.py
-@@ -230,6 +230,10 @@ class RegexURLResolver(LocaleRegexProvider):
+@@ -230,6 +230,10 @@
          self._reverse_dict = {}
          self._namespace_dict = {}
          self._app_dict = {}
@@ -53,7 +51,7 @@ index 1497d43..6a2aec7 100644
  
      def __repr__(self):
          return smart_str(u'<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern))
-@@ -240,6 +244,15 @@ class RegexURLResolver(LocaleRegexProvider):
+@@ -240,6 +244,15 @@
          apps = {}
          language_code = get_language()
          for pattern in reversed(self.url_patterns):
@@ -69,7 +67,7 @@ index 1497d43..6a2aec7 100644
              p_pattern = pattern.regex.pattern
              if p_pattern.startswith('^'):
                  p_pattern = p_pattern[1:]
-@@ -260,6 +273,7 @@ class RegexURLResolver(LocaleRegexProvider):
+@@ -260,6 +273,7 @@
                          namespaces[namespace] = (p_pattern + prefix, sub_pattern)
                      for app_name, namespace_list in pattern.app_dict.items():
                          apps.setdefault(app_name, []).extend(namespace_list)
@@ -77,7 +75,7 @@ index 1497d43..6a2aec7 100644
              else:
                  bits = normalize(p_pattern)
                  lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
-@@ -268,6 +282,7 @@ class RegexURLResolver(LocaleRegexProvider):
+@@ -268,6 +282,7 @@
          self._reverse_dict[language_code] = lookups
          self._namespace_dict[language_code] = namespaces
          self._app_dict[language_code] = apps
@@ -85,7 +83,7 @@ index 1497d43..6a2aec7 100644
  
      @property
      def reverse_dict(self):
-@@ -356,8 +371,13 @@ class RegexURLResolver(LocaleRegexProvider):
+@@ -356,8 +371,13 @@
      def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
          if args and kwargs:
              raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
@@ -100,17 +98,12 @@ index 1497d43..6a2aec7 100644
... 601 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django.git



More information about the Python-modules-commits mailing list