Chris Lamb wrote:

> The full debdiffs are attached. Can you especially check the
> versioning scheme and distribution fields for me? I often get this
> wrong and end up confusing myself. Really appreciated.

They are now attached.


Regards,

-- 
      ,''`.
     : :'  :     Chris Lamb
     `. `'`      la...@debian.org 🍥 chris-lamb.co.uk
       `-
diff --git a/debian/changelog b/debian/changelog
index a84d1b261..f18eaf3ed 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,20 @@
+python-django (1:1.10.7-2+deb9u9) stretch-security; urgency=high
+
+  * CVE-2020-13254: Potential a data leakage via malformed memcached keys.
+
+    In cases where a memcached backend does not perform key validation, passing
+    malformed cache keys could result in a key collision, and potential data
+    leakage. In order to avoid this vulnerability, key validation is added to
+    the memcached cache backends.
+
+  * CVE-2020-13596: Possible XSS via admin ForeignKeyRawIdWidget.
+
+    Query parameters to the admin ForeignKeyRawIdWidget were not properly URL
+    encoded, posing an XSS attack vector. ForeignKeyRawIdWidget now ensures
+    query parameters are correctly URL encoded.
+
+ -- Chris Lamb <la...@debian.org>  Sat, 13 Jun 2020 15:47:14 +0100
+
 python-django (1:1.10.7-2+deb9u8) stretch-security; urgency=high
 
   * CVE-2020-7471: Prevent a Potential SQL injection via StringAgg(delimiter).
diff --git a/debian/patches/0027-CVE-2020-13254.patch 
b/debian/patches/0027-CVE-2020-13254.patch
new file mode 100644
index 000000000..e2e03f982
--- /dev/null
+++ b/debian/patches/0027-CVE-2020-13254.patch
@@ -0,0 +1,177 @@
+From: Chris Lamb <la...@debian.org>
+Date: Sat, 13 Jun 2020 15:31:18 +0100
+Subject: CVE-2020-13254
+
+---
+ django/core/cache/__init__.py           |  4 ++--
+ django/core/cache/backends/base.py      | 33 +++++++++++++++++++++------------
+ django/core/cache/backends/memcached.py | 24 ++++++++++++++++++++++--
+ 3 files changed, 45 insertions(+), 16 deletions(-)
+
+diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py
+index 26897ff..dc377a9 100644
+--- a/django/core/cache/__init__.py
++++ b/django/core/cache/__init__.py
+@@ -17,13 +17,13 @@ from threading import local
+ from django.conf import settings
+ from django.core import signals
+ from django.core.cache.backends.base import (
+-    BaseCache, CacheKeyWarning, InvalidCacheBackendError,
++    BaseCache, CacheKeyWarning, InvalidCacheBackendError, InvalidCacheKey,
+ )
+ from django.utils.module_loading import import_string
+ 
+ __all__ = [
+     'cache', 'DEFAULT_CACHE_ALIAS', 'InvalidCacheBackendError',
+-    'CacheKeyWarning', 'BaseCache',
++    'CacheKeyWarning', 'BaseCache', 'InvalidCacheKey',
+ ]
+ 
+ DEFAULT_CACHE_ALIAS = 'default'
+diff --git a/django/core/cache/backends/base.py 
b/django/core/cache/backends/base.py
+index a07a34e..688ffb8 100644
+--- a/django/core/cache/backends/base.py
++++ b/django/core/cache/backends/base.py
+@@ -24,6 +24,10 @@ DEFAULT_TIMEOUT = object()
+ MEMCACHE_MAX_KEY_LENGTH = 250
+ 
+ 
++class InvalidCacheKey(ValueError):
++    pass
++
++
+ def default_key_func(key, key_prefix, version):
+     """
+     Default function to generate keys.
+@@ -233,18 +237,8 @@ class BaseCache(object):
+         backend. This encourages (but does not force) writing backend-portable
+         cache code.
+         """
+-        if len(key) > MEMCACHE_MAX_KEY_LENGTH:
+-            warnings.warn(
+-                'Cache key will cause errors if used with memcached: %r '
+-                '(longer than %s)' % (key, MEMCACHE_MAX_KEY_LENGTH), 
CacheKeyWarning
+-            )
+-        for char in key:
+-            if ord(char) < 33 or ord(char) == 127:
+-                warnings.warn(
+-                    'Cache key contains characters that will cause errors if '
+-                    'used with memcached: %r' % key, CacheKeyWarning
+-                )
+-                break
++        for warning in memcache_key_warnings(key):
++            warnings.warn(warning, CacheKeyWarning)
+ 
+     def incr_version(self, key, delta=1, version=None):
+         """Adds delta to the cache version for the supplied key. Returns the
+@@ -270,3 +264,18 @@ class BaseCache(object):
+     def close(self, **kwargs):
+         """Close the cache connection"""
+         pass
++
++
++def memcache_key_warnings(key):
++    if len(key) > MEMCACHE_MAX_KEY_LENGTH:
++        yield (
++            'Cache key will cause errors if used with memcached: %r '
++            '(longer than %s)' % (key, MEMCACHE_MAX_KEY_LENGTH)
++        )
++    for char in key:
++        if ord(char) < 33 or ord(char) == 127:
++            yield (
++                'Cache key contains characters that will cause errors if '
++                'used with memcached: %r' % key,
++            )
++            break
+diff --git a/django/core/cache/backends/memcached.py 
b/django/core/cache/backends/memcached.py
+index ee6b3b7..80395e6 100644
+--- a/django/core/cache/backends/memcached.py
++++ b/django/core/cache/backends/memcached.py
+@@ -3,7 +3,9 @@
+ import pickle
+ import time
+ 
+-from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
++from django.core.cache.backends.base import (
++    DEFAULT_TIMEOUT, BaseCache, InvalidCacheKey, memcache_key_warnings,
++)
+ from django.utils import six
+ from django.utils.encoding import force_str
+ from django.utils.functional import cached_property
+@@ -69,10 +71,12 @@ class BaseMemcachedCache(BaseCache):
+ 
+     def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         return self._cache.add(key, value, self.get_backend_timeout(timeout))
+ 
+     def get(self, key, default=None, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         val = self._cache.get(key)
+         if val is None:
+             return default
+@@ -80,16 +84,20 @@ class BaseMemcachedCache(BaseCache):
+ 
+     def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         if not self._cache.set(key, value, self.get_backend_timeout(timeout)):
+             # make sure the key doesn't keep its old value in case of failure 
to set (memcached's 1MB limit)
+             self._cache.delete(key)
+ 
+     def delete(self, key, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         self._cache.delete(key)
+ 
+     def get_many(self, keys, version=None):
+         new_keys = [self.make_key(x, version=version) for x in keys]
++        for key in new_keys:
++            self.validate_key(key)
+         ret = self._cache.get_multi(new_keys)
+         if ret:
+             _ = {}
+@@ -104,6 +112,7 @@ class BaseMemcachedCache(BaseCache):
+ 
+     def incr(self, key, delta=1, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         # memcached doesn't support a negative delta
+         if delta < 0:
+             return self._cache.decr(key, -delta)
+@@ -122,6 +131,7 @@ class BaseMemcachedCache(BaseCache):
+ 
+     def decr(self, key, delta=1, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         # memcached doesn't support a negative delta
+         if delta < 0:
+             return self._cache.incr(key, -delta)
+@@ -142,15 +152,25 @@ class BaseMemcachedCache(BaseCache):
+         safe_data = {}
+         for key, value in data.items():
+             key = self.make_key(key, version=version)
++            self.validate_key(key)
+             safe_data[key] = value
+         self._cache.set_multi(safe_data, self.get_backend_timeout(timeout))
+ 
+     def delete_many(self, keys, version=None):
+-        self._cache.delete_multi(self.make_key(key, version=version) for key 
in keys)
++        to_delete = []
++        for key in to_delete:
++            key = self.make_key(key, version=version)
++            self.validate_key(key)
++            to_delete.append(key)
++        self._cache.delete_multi(to_delete)
+ 
+     def clear(self):
+         self._cache.flush_all()
+ 
++    def validate_key(self, key):
++        for warning in memcache_key_warnings(key):
++            raise InvalidCacheKey(warning)
++
+ 
+ class MemcachedCache(BaseMemcachedCache):
+     "An implementation of a cache binding using python-memcached"
diff --git a/debian/patches/0028-CVE-2020-13596.patch 
b/debian/patches/0028-CVE-2020-13596.patch
new file mode 100644
index 000000000..e415c0f2e
--- /dev/null
+++ b/debian/patches/0028-CVE-2020-13596.patch
@@ -0,0 +1,29 @@
+From: Chris Lamb <la...@debian.org>
+Date: Sat, 13 Jun 2020 15:31:58 +0100
+Subject: CVE-2020-13596
+
+---
+ django/contrib/admin/widgets.py | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
+index 26de9c7..64f3af5 100644
+--- a/django/contrib/admin/widgets.py
++++ b/django/contrib/admin/widgets.py
+@@ -15,6 +15,7 @@ from django.template.loader import render_to_string
+ from django.urls import reverse
+ from django.urls.exceptions import NoReverseMatch
+ from django.utils import six
++from django.utils.http import urlencode
+ from django.utils.encoding import force_text
+ from django.utils.html import format_html, format_html_join, smart_urlquote
+ from django.utils.safestring import mark_safe
+@@ -166,7 +167,7 @@ class ForeignKeyRawIdWidget(forms.TextInput):
+ 
+             params = self.url_parameters()
+             if params:
+-                url = '?' + '&amp;'.join('%s=%s' % (k, v) for k, v in 
params.items())
++                url = '?' + urlencode(params)
+             else:
+                 url = ''
+             if "class" not in attrs:
diff --git a/debian/patches/series b/debian/patches/series
index 14a02714a..f4976f08a 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -18,3 +18,5 @@ fix-test-middleware-classes-headers.patch
 0024-CVE-2019-14235.patch
 0025-CVE-2019-19844.patch
 0026-CVE-2020-7471.patch
+0027-CVE-2020-13254.patch
+0028-CVE-2020-13596.patch
diff --git a/Django.egg-info/PKG-INFO b/Django.egg-info/PKG-INFO
index 67176d082..9fc763d6d 100644
--- a/Django.egg-info/PKG-INFO
+++ b/Django.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: Django
-Version: 1.11.28
+Version: 1.11.29
 Summary: A high-level Python Web framework that encourages rapid development 
and clean, pragmatic design.
 Home-page: https://www.djangoproject.com/
 Author: Django Software Foundation
@@ -27,5 +27,5 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
 Classifier: Topic :: Software Development :: Libraries :: Application 
Frameworks
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Provides-Extra: bcrypt
 Provides-Extra: argon2
+Provides-Extra: bcrypt
diff --git a/Django.egg-info/SOURCES.txt b/Django.egg-info/SOURCES.txt
index 69b7fcbdb..c0501ba36 100644
--- a/Django.egg-info/SOURCES.txt
+++ b/Django.egg-info/SOURCES.txt
@@ -3556,6 +3556,7 @@ docs/releases/1.11.25.txt
 docs/releases/1.11.26.txt
 docs/releases/1.11.27.txt
 docs/releases/1.11.28.txt
+docs/releases/1.11.29.txt
 docs/releases/1.11.3.txt
 docs/releases/1.11.4.txt
 docs/releases/1.11.5.txt
diff --git a/PKG-INFO b/PKG-INFO
index 67176d082..9fc763d6d 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: Django
-Version: 1.11.28
+Version: 1.11.29
 Summary: A high-level Python Web framework that encourages rapid development 
and clean, pragmatic design.
 Home-page: https://www.djangoproject.com/
 Author: Django Software Foundation
@@ -27,5 +27,5 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
 Classifier: Topic :: Software Development :: Libraries :: Application 
Frameworks
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Provides-Extra: bcrypt
 Provides-Extra: argon2
+Provides-Extra: bcrypt
diff --git a/debian/changelog b/debian/changelog
index f36964c1b..00bbc0532 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,32 @@
+python-django (1:1.11.29-1~deb10u1) buster-security; urgency=high
+
+  * New upstream security release (postponed from March 2020):
+
+    - CVE-2020-9402: Potential SQL injection via tolerance parameter in GIS
+      functions and aggregates on Oracle
+
+    Note that Django 1.11.x left upstream's extended security support on April
+    1st 2020. For more information, please see:
+
+      https://www.djangoproject.com/download/
+
+  * This upload also fixes the following security issues:
+
+    - CVE-2020-13254: Potential a data leakage via malformed memcached keys.
+
+      In cases where a memcached backend does not perform key validation,
+      passing malformed cache keys could result in a key collision, and
+      potential data leakage. In order to avoid this vulnerability, key
+      validation is added to the memcached cache backends.
+
+    - CVE-2020-13596: Possible XSS via admin ForeignKeyRawIdWidget.
+
+      Query parameters to the admin ForeignKeyRawIdWidget were not properly URL
+      encoded, posing an XSS attack vector. ForeignKeyRawIdWidget now ensures
+      query parameters are correctly URL encoded.
+
+ -- Chris Lamb <la...@debian.org>  Sun, 14 Jun 2020 12:15:26 +0100
+
 python-django (1:1.11.28-1~deb10u1) buster-security; urgency=high
 
   * New upstream security release. (Closes: #950581)
diff --git a/debian/patches/0008-CVE-2020-13254.patch 
b/debian/patches/0008-CVE-2020-13254.patch
new file mode 100644
index 000000000..79e842bed
--- /dev/null
+++ b/debian/patches/0008-CVE-2020-13254.patch
@@ -0,0 +1,174 @@
+From: Chris Lamb <la...@debian.org>
+Date: Sun, 14 Jun 2020 12:08:06 +0100
+Subject: CVE-2020-13254
+
+---
+ django/core/cache/__init__.py           |  3 ++-
+ django/core/cache/backends/base.py      | 33 +++++++++++++++++++++------------
+ django/core/cache/backends/memcached.py | 22 ++++++++++++++++++++--
+ 3 files changed, 43 insertions(+), 15 deletions(-)
+
+diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py
+index cd2bb43..bc1e4de 100644
+--- a/django/core/cache/__init__.py
++++ b/django/core/cache/__init__.py
+@@ -18,12 +18,13 @@ from django.conf import settings
+ from django.core import signals
+ from django.core.cache.backends.base import (
+     BaseCache, CacheKeyWarning, InvalidCacheBackendError,
++    InvalidCacheKey,
+ )
+ from django.utils.module_loading import import_string
+ 
+ __all__ = [
+     'cache', 'DEFAULT_CACHE_ALIAS', 'InvalidCacheBackendError',
+-    'CacheKeyWarning', 'BaseCache',
++    'CacheKeyWarning', 'BaseCache', 'InvalidCacheKey',
+ ]
+ 
+ DEFAULT_CACHE_ALIAS = 'default'
+diff --git a/django/core/cache/backends/base.py 
b/django/core/cache/backends/base.py
+index 1235f7e..db8cc37 100644
+--- a/django/core/cache/backends/base.py
++++ b/django/core/cache/backends/base.py
+@@ -16,6 +16,10 @@ class CacheKeyWarning(DjangoRuntimeWarning):
+     pass
+ 
+ 
++class InvalidCacheKey(ValueError):
++    pass
++
++
+ # Stub class to ensure not passing in a `timeout` argument results in
+ # the default timeout
+ DEFAULT_TIMEOUT = object()
+@@ -233,18 +237,8 @@ class BaseCache(object):
+         backend. This encourages (but does not force) writing backend-portable
+         cache code.
+         """
+-        if len(key) > MEMCACHE_MAX_KEY_LENGTH:
+-            warnings.warn(
+-                'Cache key will cause errors if used with memcached: %r '
+-                '(longer than %s)' % (key, MEMCACHE_MAX_KEY_LENGTH), 
CacheKeyWarning
+-            )
+-        for char in key:
+-            if ord(char) < 33 or ord(char) == 127:
+-                warnings.warn(
+-                    'Cache key contains characters that will cause errors if '
+-                    'used with memcached: %r' % key, CacheKeyWarning
+-                )
+-                break
++        for warning in memcache_key_warnings(key):
++            warnings.warn(warning, CacheKeyWarning)
+ 
+     def incr_version(self, key, delta=1, version=None):
+         """Adds delta to the cache version for the supplied key. Returns the
+@@ -270,3 +264,18 @@ class BaseCache(object):
+     def close(self, **kwargs):
+         """Close the cache connection"""
+         pass
++
++
++def memcache_key_warnings(key):
++    if len(key) > MEMCACHE_MAX_KEY_LENGTH:
++        yield (
++            'Cache key will cause errors if used with memcached: %r '
++            '(longer than %s)' % (key, MEMCACHE_MAX_KEY_LENGTH)
++        )
++    for char in key:
++        if ord(char) < 33 or ord(char) == 127:
++            yield (
++                'Cache key contains characters that will cause errors if '
++                'used with memcached: %r' % key
++            )
++            break
+diff --git a/django/core/cache/backends/memcached.py 
b/django/core/cache/backends/memcached.py
+index 4cf25fb..b77ea30 100644
+--- a/django/core/cache/backends/memcached.py
++++ b/django/core/cache/backends/memcached.py
+@@ -5,7 +5,9 @@ import re
+ import time
+ import warnings
+ 
+-from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
++from django.core.cache.backends.base import (
++    DEFAULT_TIMEOUT, BaseCache, InvalidCacheKey, memcache_key_warnings,
++)
+ from django.utils import six
+ from django.utils.deprecation import RemovedInDjango21Warning
+ from django.utils.encoding import force_str
+@@ -72,10 +74,12 @@ class BaseMemcachedCache(BaseCache):
+ 
+     def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         return self._cache.add(key, value, self.get_backend_timeout(timeout))
+ 
+     def get(self, key, default=None, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         val = self._cache.get(key)
+         if val is None:
+             return default
+@@ -83,16 +87,20 @@ class BaseMemcachedCache(BaseCache):
+ 
+     def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         if not self._cache.set(key, value, self.get_backend_timeout(timeout)):
+             # make sure the key doesn't keep its old value in case of failure 
to set (memcached's 1MB limit)
+             self._cache.delete(key)
+ 
+     def delete(self, key, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         self._cache.delete(key)
+ 
+     def get_many(self, keys, version=None):
+         new_keys = [self.make_key(x, version=version) for x in keys]
++        for key in new_keys:
++            self.validate_key(key)
+         ret = self._cache.get_multi(new_keys)
+         if ret:
+             _ = {}
+@@ -108,6 +116,7 @@ class BaseMemcachedCache(BaseCache):
+ 
+     def incr(self, key, delta=1, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         # memcached doesn't support a negative delta
+         if delta < 0:
+             return self._cache.decr(key, -delta)
+@@ -126,6 +135,7 @@ class BaseMemcachedCache(BaseCache):
+ 
+     def decr(self, key, delta=1, version=None):
+         key = self.make_key(key, version=version)
++        self.validate_key(key)
+         # memcached doesn't support a negative delta
+         if delta < 0:
+             return self._cache.incr(key, -delta)
+@@ -146,15 +156,23 @@ class BaseMemcachedCache(BaseCache):
+         safe_data = {}
+         for key, value in data.items():
+             key = self.make_key(key, version=version)
++            self.validate_key(key)
+             safe_data[key] = value
+         self._cache.set_multi(safe_data, self.get_backend_timeout(timeout))
+ 
+     def delete_many(self, keys, version=None):
+-        self._cache.delete_multi(self.make_key(key, version=version) for key 
in keys)
++        keys = [self.make_key(key, version=version) for key in keys]
++        for key in keys:
++            self.validate_key(key)
++        self._cache.delete_multi(keys)
+ 
+     def clear(self):
+         self._cache.flush_all()
+ 
++    def validate_key(self, key):
++        for warning in memcache_key_warnings(key):
++            raise InvalidCacheKey(warning)
++
+ 
+ class MemcachedCache(BaseMemcachedCache):
+     "An implementation of a cache binding using python-memcached"
diff --git a/debian/patches/0009-CVE-2020-13596.patch 
b/debian/patches/0009-CVE-2020-13596.patch
new file mode 100644
index 000000000..1138c7a8c
--- /dev/null
+++ b/debian/patches/0009-CVE-2020-13596.patch
@@ -0,0 +1,29 @@
+From: Chris Lamb <la...@debian.org>
+Date: Tue, 9 Jun 2020 15:55:54 +0100
+Subject: CVE-2020-13596
+
+---
+ django/contrib/admin/widgets.py | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
+index 209e028..1a96fa7 100644
+--- a/django/contrib/admin/widgets.py
++++ b/django/contrib/admin/widgets.py
+@@ -14,6 +14,7 @@ from django.urls.exceptions import NoReverseMatch
+ from django.utils import six
+ from django.utils.encoding import force_text
+ from django.utils.html import smart_urlquote
++from django.utils.http import urlencode
+ from django.utils.safestring import mark_safe
+ from django.utils.text import Truncator
+ from django.utils.translation import ugettext as _
+@@ -149,7 +150,7 @@ class ForeignKeyRawIdWidget(forms.TextInput):
+ 
+             params = self.url_parameters()
+             if params:
+-                related_url += '?' + '&amp;'.join('%s=%s' % (k, v) for k, v 
in params.items())
++                related_url += '?' + urlencode(params)
+             context['related_url'] = mark_safe(related_url)
+             context['link_title'] = _('Lookup')
+             # The JavaScript code looks for this class.
diff --git a/debian/patches/series b/debian/patches/series
index 59611e9f1..296032c78 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -4,3 +4,5 @@
 0004-Fix-QuerySet.defer-with-super-and-subclass-fields.patch
 0006-Default-to-supporting-Spatialite-4.2.patch
 0007-Fixed-29182-Adjusted-SQLite-schema-table-alteration-.patch
+0008-CVE-2020-13254.patch
+0009-CVE-2020-13596.patch
diff --git a/django/__init__.py b/django/__init__.py
index a5c5ca149..b683b5e0e 100644
--- a/django/__init__.py
+++ b/django/__init__.py
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
 
 from django.utils.version import get_version
 
-VERSION = (1, 11, 28, 'final', 0)
+VERSION = (1, 11, 29, 'final', 0)
 
 __version__ = get_version(VERSION)
 
diff --git a/django/contrib/gis/db/models/aggregates.py 
b/django/contrib/gis/db/models/aggregates.py
index 416481f9c..0e077550a 100644
--- a/django/contrib/gis/db/models/aggregates.py
+++ b/django/contrib/gis/db/models/aggregates.py
@@ -1,4 +1,5 @@
 from django.contrib.gis.db.models.fields import ExtentField
+from django.db.models import Value
 from django.db.models.aggregates import Aggregate
 
 __all__ = ['Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union']
@@ -16,11 +17,14 @@ class GeoAggregate(Aggregate):
         return super(GeoAggregate, self).as_sql(compiler, connection)
 
     def as_oracle(self, compiler, connection):
-        if not hasattr(self, 'tolerance'):
-            self.tolerance = 0.05
-        self.extra['tolerance'] = self.tolerance
         if not self.is_extent:
-            self.template = 
'%(function)s(SDOAGGRTYPE(%(expressions)s,%(tolerance)s))'
+            tolerance = self.extra.get('tolerance') or getattr(self, 
'tolerance', 0.05)
+            clone = self.copy()
+            expressions = clone.get_source_expressions()
+            expressions.append(Value(tolerance))
+            clone.set_source_expressions(expressions)
+            clone.template = '%(function)s(SDOAGGRTYPE(%(expressions)s))'
+            return clone.as_sql(compiler, connection)
         return self.as_sql(compiler, connection)
 
     def resolve_expression(self, query=None, allow_joins=True, reuse=None, 
summarize=False, for_save=False):
diff --git a/django/contrib/gis/db/models/functions.py 
b/django/contrib/gis/db/models/functions.py
index 95eda246a..ef9e11818 100644
--- a/django/contrib/gis/db/models/functions.py
+++ b/django/contrib/gis/db/models/functions.py
@@ -117,7 +117,11 @@ class OracleToleranceMixin(object):
     tolerance = 0.05
 
     def as_oracle(self, compiler, connection):
-        tol = self.extra.get('tolerance', self.tolerance)
+        tol = self._handle_param(
+            self.extra.get('tolerance', self.tolerance),
+            'tolerance',
+            NUMERIC_TYPES,
+        )
         self.template = "%%(function)s(%%(expressions)s, %s)" % tol
         return super(OracleToleranceMixin, self).as_sql(compiler, connection)
 
diff --git a/docs/releases/1.11.29.txt b/docs/releases/1.11.29.txt
new file mode 100644
index 000000000..d37f3ffc0
--- /dev/null
+++ b/docs/releases/1.11.29.txt
@@ -0,0 +1,13 @@
+============================
+Django 1.11.29 release notes
+============================
+
+*March 4, 2020*
+
+Django 1.11.29 fixes a security issue in 1.11.29.
+
+CVE-2020-9402: Potential SQL injection via ``tolerance`` parameter in GIS 
functions and aggregates on Oracle
+============================================================================================================
+
+GIS functions and aggregates on Oracle were subject to SQL injection,
+using a suitably crafted ``tolerance``.
diff --git a/docs/releases/index.txt b/docs/releases/index.txt
index c2e913caf..be5fb3e54 100644
--- a/docs/releases/index.txt
+++ b/docs/releases/index.txt
@@ -26,6 +26,7 @@ versions of the documentation contain the release notes for 
any later releases.
 .. toctree::
    :maxdepth: 1
 
+   1.11.29
    1.11.28
    1.11.27
    1.11.26
diff --git a/docs/releases/security.txt b/docs/releases/security.txt
index d461ce3d8..8b705e90d 100644
--- a/docs/releases/security.txt
+++ b/docs/releases/security.txt
@@ -1042,3 +1042,16 @@ Versions affected
 * Django 3.0 :commit:`(patch) <302a4ff1e8b1c798aab97673909c7a3dfda42c26>`
 * Django 2.2 :commit:`(patch) <4d334bea06cac63dc1272abcec545b85136cca0e>`
 * Django 1.11 :commit:`(patch) <f4cff43bf921fcea6a29b726eb66767f67753fa2>`
+
+February 3, 2020 - :cve:`2020-7471`
+-----------------------------------
+
+Potential SQL injection via ``StringAgg(delimiter)``. `Full description
+<https://www.djangoproject.com/weblog/2020/feb/03/security-releases/>`__
+
+Versions affected
+~~~~~~~~~~~~~~~~~
+
+* Django 3.0 :commit:`(patch) <505826b469b16ab36693360da9e11fd13213421b>`
+* Django 2.2 :commit:`(patch) <c67a368c16e4680b324b4f385398d638db4d8147>`
+* Django 1.11 :commit:`(patch) <001b0634cd309e372edb6d7d95d083d02b8e37bd>`
diff --git a/tests/gis_tests/distapp/tests.py b/tests/gis_tests/distapp/tests.py
index 4609083f3..dd7f9efe6 100644
--- a/tests/gis_tests/distapp/tests.py
+++ b/tests/gis_tests/distapp/tests.py
@@ -1,6 +1,6 @@
 from __future__ import unicode_literals
 
-from unittest import skipIf
+from unittest import skipIf, skipUnless
 
 from django.contrib.gis.db.models.functions import (
     Area, Distance, Length, Perimeter, Transform,
@@ -588,6 +588,37 @@ class DistanceFunctionsTests(TestCase):
             for i, c in enumerate(qs):
                 self.assertAlmostEqual(sphere_distances[i], c.distance.m, tol)
 
+    @skipUnless(
+        connection.vendor == 'oracle',
+        'Oracle supports tolerance paremeter.',
+    )
+    def test_distance_function_tolerance_escaping(self):
+        qs = AustraliaCity.objects.annotate(
+            d=Distance(
+                'point',
+                Point(0, 0, srid=3857),
+                tolerance='0.05) = 1 OR 1=1 OR (1+1',
+            ),
+        ).filter(d=1).values('pk')
+        msg = 'The tolerance parameter has the wrong type'
+        with self.assertRaisesMessage(TypeError, msg):
+            qs.exists()
+
+    @skipUnless(
+        connection.vendor == 'oracle',
+        'Oracle supports tolerance paremeter.',
+    )
+    def test_distance_function_tolerance(self):
+        # Tolerance is greater than distance.
+        qs = AustraliaCity.objects.annotate(
+            d=Distance(
+                'point',
+                Point(151.23, -33.95, srid=4326),
+                tolerance=340.7,
+            ),
+        ).filter(d=0).values('pk')
+        self.assertIs(qs.exists(), True)
+
     @no_oracle  # Oracle already handles geographic distance calculation.
     @skipUnlessDBFeature("has_Distance_function", 'has_Transform_function')
     def test_distance_transform(self):
diff --git a/tests/gis_tests/geoapp/tests.py b/tests/gis_tests/geoapp/tests.py
index b6a5c2d0c..7eff87693 100644
--- a/tests/gis_tests/geoapp/tests.py
+++ b/tests/gis_tests/geoapp/tests.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 import re
 import tempfile
+import unittest
 
 from django.contrib.gis import gdal
 from django.contrib.gis.db.models import Extent, MakeLine, Union
@@ -10,7 +11,7 @@ from django.contrib.gis.geos import (
     MultiPoint, MultiPolygon, Point, Polygon, fromstr,
 )
 from django.core.management import call_command
-from django.db import connection
+from django.db import DatabaseError, connection
 from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
 from django.utils import six
 from django.utils.deprecation import RemovedInDjango20Warning
@@ -881,6 +882,42 @@ class GeoQuerySetTest(TestCase):
         qs = City.objects.filter(name='NotACity')
         self.assertIsNone(qs.aggregate(Union('point'))['point__union'])
 
+    @unittest.skipUnless(
+        connection.vendor == 'oracle',
+        'Oracle supports tolerance paremeter.',
+    )
+    def test_unionagg_tolerance(self):
+        City.objects.create(
+            point=fromstr('POINT(-96.467222 32.751389)', srid=4326),
+            name='Forney',
+        )
+        tx = Country.objects.get(name='Texas').mpoly
+        # Tolerance is greater than distance between Forney and Dallas, that's
+        # why Dallas is ignored.
+        forney_houston = GEOSGeometry(
+            'MULTIPOINT(-95.363151 29.763374, -96.467222 32.751389)',
+            srid=4326,
+        )
+        self.assertIs(
+            forney_houston.equals(
+                City.objects.filter(point__within=tx).aggregate(
+                    Union('point', tolerance=32000),
+                )['point__union'],
+            ),
+            True,
+        )
+
+    @unittest.skipUnless(
+        connection.vendor == 'oracle',
+        'Oracle supports tolerance paremeter.',
+    )
+    def test_unionagg_tolerance_escaping(self):
+        tx = Country.objects.get(name='Texas').mpoly
+        with self.assertRaises(DatabaseError):
+            City.objects.filter(point__within=tx).aggregate(
+                Union('point', tolerance='0.05))), (((1'),
+            )
+
     def test_within_subquery(self):
         """
         Using a queryset inside a geo lookup is working (using a subquery)
diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt
index 08a6b3a52..d323dbbf5 100644
--- a/tests/requirements/base.txt
+++ b/tests/requirements/base.txt
@@ -5,7 +5,7 @@ geoip2
 jinja2 >= 2.9.2
 numpy
 Pillow != 5.4.0
-PyYAML
+PyYAML < 5.3
 # pylibmc/libmemcached can't be built on Windows.
 pylibmc; sys.platform != 'win32'
 python-memcached >= 1.59

Reply via email to