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 = '?' + '&'.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 += '?' + '&'.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