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 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 +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 +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