commit dca6df3ed1d10f122917086529a925824ed17023
Author: Dan Palmer <dan@danpalmer.me>
Date:   Wed May 20 11:45:31 2020 +0200

    [2.2.x] Fixed CVE-2020-13254 -- Enforced cache key validation in memcached backends.

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.

Index: Django-1.11.24/django/core/cache/__init__.py
===================================================================
--- Django-1.11.24.orig/django/core/cache/__init__.py
+++ Django-1.11.24/django/core/cache/__init__.py
@@ -17,13 +17,12 @@ from threading import local
 from django.conf import settings
 from django.core import signals
 from django.core.cache.backends.base import (
-    BaseCache, CacheKeyWarning, InvalidCacheBackendError,
-)
+    InvalidCacheBackendError, InvalidCacheKey, CacheKeyWarning, BaseCache)
 from django.utils.module_loading import import_string
 
 __all__ = [
     'cache', 'DEFAULT_CACHE_ALIAS', 'InvalidCacheBackendError',
-    'CacheKeyWarning', 'BaseCache',
+    'CacheKeyWarning', 'BaseCache',  'InvalidCacheKey',
 ]
 
 DEFAULT_CACHE_ALIAS = 'default'
Index: Django-1.11.24/django/core/cache/backends/base.py
===================================================================
--- Django-1.11.24.orig/django/core/cache/backends/base.py
+++ Django-1.11.24/django/core/cache/backends/base.py
@@ -16,6 +16,10 @@ class CacheKeyWarning(DjangoRuntimeWarni
     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, CacheKeyWarning
+            )
+            break
Index: Django-1.11.24/django/core/cache/backends/memcached.py
===================================================================
--- Django-1.11.24.orig/django/core/cache/backends/memcached.py
+++ Django-1.11.24/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,6 +156,7 @@ 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))
 
@@ -155,6 +166,10 @@ class BaseMemcachedCache(BaseCache):
     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"
Index: Django-1.11.24/tests/cache/tests.py
===================================================================
--- Django-1.11.24.orig/tests/cache/tests.py
+++ Django-1.11.24/tests/cache/tests.py
@@ -18,7 +18,7 @@ import warnings
 from django.conf import settings
 from django.core import management, signals
 from django.core.cache import (
-    DEFAULT_CACHE_ALIAS, CacheKeyWarning, cache, caches,
+    DEFAULT_CACHE_ALIAS, CacheKeyWarning, InvalidCacheKey, cache, caches,
 )
 from django.core.cache.utils import make_template_fragment_key
 from django.db import close_old_connections, connection, connections
@@ -577,10 +577,10 @@ class BaseCacheTests(object):
 
     def _perform_invalid_key_test(self, key, expected_warning):
         """
-        All the builtin backends (except memcached, see below) should warn on
-        keys that would be refused by memcached. This encourages portable
-        caching code without making it too difficult to use production backends
-        with more liberal key rules. Refs #6447.
+        All the builtin backends should warn (except memcached that should
+        error) on keys that would be refused by memcached. This encourages
+        portable caching code without making it too difficult to use production
+        backends with more liberal key rules. Refs #6447.
         """
         # mimic custom ``make_key`` method being defined since the default will
         # never show the below warnings
@@ -1178,24 +1178,14 @@ class BaseMemcachedTests(BaseCacheTests)
             with self.settings(CACHES={'default': params}):
                 self.assertEqual(cache._servers, ['server1.tld', 'server2:11211'])
 
-    def test_invalid_key_characters(self):
+    def _perform_invalid_key_test(self, key, expected_warning):
         """
-        On memcached, we don't introduce a duplicate key validation
-        step (for speed reasons), we just let the memcached API
-        library raise its own exception on bad keys. Refs #6447.
-
-        In order to be memcached-API-library agnostic, we only assert
-        that a generic exception of some kind is raised.
+        Whilst other backends merely warn, memcached should raise for an
+        invalid key.
         """
-        # memcached does not allow whitespace or control characters in keys
-        # when using the ascii protocol.
-        with self.assertRaises(Exception):
-            cache.set('key with spaces', 'value')
-
-    def test_invalid_key_length(self):
-        # memcached limits key length to 250
-        with self.assertRaises(Exception):
-            cache.set('a' * 251, 'value')
+        msg = expected_warning.replace(key, ':1:%s' % key)
+        with self.assertRaisesMessage(InvalidCacheKey, msg):
+            cache.set(key, 'value')
 
     def test_default_never_expiring_timeout(self):
         # Regression test for #22845
@@ -1294,15 +1284,6 @@ class PyLibMCCacheTests(BaseMemcachedTes
     # libmemcached manages its own connections.
     should_disconnect_on_close = False
 
-    # By default, pylibmc/libmemcached don't verify keys client-side and so
-    # this test triggers a server-side bug that causes later tests to fail
-    # (#19914). The `verify_keys` behavior option could be set to True (which
-    # would avoid triggering the server-side bug), however this test would
-    # still fail due to https://github.com/lericson/pylibmc/issues/219.
-    @unittest.skip("triggers a memcached-server bug, causing subsequent tests to fail")
-    def test_invalid_key_characters(self):
-        pass
-
     @override_settings(CACHES=caches_setting_for_tests(
         base=PyLibMCCache_params,
         exclude=memcached_excluded_caches,
