Skip to content

Commit 07e59ca

Browse files
danpalmercarltongibson
authored andcommitted
[2.2.x] Fixed CVE-2020-13254 -- Enforced cache key validation in memcached backends.
1 parent 6d61860 commit 07e59ca

File tree

5 files changed

+58
-45
lines changed

5 files changed

+58
-45
lines changed

django/core/cache/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
from django.conf import settings
1818
from django.core import signals
1919
from django.core.cache.backends.base import (
20-
BaseCache, CacheKeyWarning, InvalidCacheBackendError,
20+
BaseCache, CacheKeyWarning, InvalidCacheBackendError, InvalidCacheKey,
2121
)
2222
from django.utils.module_loading import import_string
2323

2424
__all__ = [
2525
'cache', 'caches', 'DEFAULT_CACHE_ALIAS', 'InvalidCacheBackendError',
26-
'CacheKeyWarning', 'BaseCache',
26+
'CacheKeyWarning', 'BaseCache', 'InvalidCacheKey',
2727
]
2828

2929
DEFAULT_CACHE_ALIAS = 'default'

django/core/cache/backends/base.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class CacheKeyWarning(RuntimeWarning):
1414
pass
1515

1616

17+
class InvalidCacheKey(ValueError):
18+
pass
19+
20+
1721
# Stub class to ensure not passing in a `timeout` argument results in
1822
# the default timeout
1923
DEFAULT_TIMEOUT = object()
@@ -242,18 +246,8 @@ def validate_key(self, key):
242246
backend. This encourages (but does not force) writing backend-portable
243247
cache code.
244248
"""
245-
if len(key) > MEMCACHE_MAX_KEY_LENGTH:
246-
warnings.warn(
247-
'Cache key will cause errors if used with memcached: %r '
248-
'(longer than %s)' % (key, MEMCACHE_MAX_KEY_LENGTH), CacheKeyWarning
249-
)
250-
for char in key:
251-
if ord(char) < 33 or ord(char) == 127:
252-
warnings.warn(
253-
'Cache key contains characters that will cause errors if '
254-
'used with memcached: %r' % key, CacheKeyWarning
255-
)
256-
break
249+
for warning in memcache_key_warnings(key):
250+
warnings.warn(warning, CacheKeyWarning)
257251

258252
def incr_version(self, key, delta=1, version=None):
259253
"""
@@ -281,3 +275,18 @@ def decr_version(self, key, delta=1, version=None):
281275
def close(self, **kwargs):
282276
"""Close the cache connection"""
283277
pass
278+
279+
280+
def memcache_key_warnings(key):
281+
if len(key) > MEMCACHE_MAX_KEY_LENGTH:
282+
yield (
283+
'Cache key will cause errors if used with memcached: %r '
284+
'(longer than %s)' % (key, MEMCACHE_MAX_KEY_LENGTH)
285+
)
286+
for char in key:
287+
if ord(char) < 33 or ord(char) == 127:
288+
yield (
289+
'Cache key contains characters that will cause errors if '
290+
'used with memcached: %r' % key, CacheKeyWarning
291+
)
292+
break

django/core/cache/backends/memcached.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import re
55
import time
66

7-
from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
7+
from django.core.cache.backends.base import (
8+
DEFAULT_TIMEOUT, BaseCache, InvalidCacheKey, memcache_key_warnings,
9+
)
810
from django.utils.functional import cached_property
911

1012

@@ -64,27 +66,33 @@ def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT):
6466

6567
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
6668
key = self.make_key(key, version=version)
69+
self.validate_key(key)
6770
return self._cache.add(key, value, self.get_backend_timeout(timeout))
6871

6972
def get(self, key, default=None, version=None):
7073
key = self.make_key(key, version=version)
74+
self.validate_key(key)
7175
val = self._cache.get(key)
7276
if val is None:
7377
return default
7478
return val
7579

7680
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
7781
key = self.make_key(key, version=version)
82+
self.validate_key(key)
7883
if not self._cache.set(key, value, self.get_backend_timeout(timeout)):
7984
# make sure the key doesn't keep its old value in case of failure to set (memcached's 1MB limit)
8085
self._cache.delete(key)
8186

8287
def delete(self, key, version=None):
8388
key = self.make_key(key, version=version)
89+
self.validate_key(key)
8490
self._cache.delete(key)
8591

8692
def get_many(self, keys, version=None):
8793
key_map = {self.make_key(key, version=version): key for key in keys}
94+
for key in key_map:
95+
self.validate_key(key)
8896
ret = self._cache.get_multi(key_map.keys())
8997
return {key_map[k]: v for k, v in ret.items()}
9098

@@ -94,6 +102,7 @@ def close(self, **kwargs):
94102

95103
def incr(self, key, delta=1, version=None):
96104
key = self.make_key(key, version=version)
105+
self.validate_key(key)
97106
# memcached doesn't support a negative delta
98107
if delta < 0:
99108
return self._cache.decr(key, -delta)
@@ -112,6 +121,7 @@ def incr(self, key, delta=1, version=None):
112121

113122
def decr(self, key, delta=1, version=None):
114123
key = self.make_key(key, version=version)
124+
self.validate_key(key)
115125
# memcached doesn't support a negative delta
116126
if delta < 0:
117127
return self._cache.incr(key, -delta)
@@ -133,6 +143,7 @@ def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
133143
original_keys = {}
134144
for key, value in data.items():
135145
safe_key = self.make_key(key, version=version)
146+
self.validate_key(safe_key)
136147
safe_data[safe_key] = value
137148
original_keys[safe_key] = key
138149
failed_keys = self._cache.set_multi(safe_data, self.get_backend_timeout(timeout))
@@ -144,6 +155,10 @@ def delete_many(self, keys, version=None):
144155
def clear(self):
145156
self._cache.flush_all()
146157

158+
def validate_key(self, key):
159+
for warning in memcache_key_warnings(key):
160+
raise InvalidCacheKey(warning)
161+
147162

148163
class MemcachedCache(BaseMemcachedCache):
149164
"An implementation of a cache binding using python-memcached"

docs/releases/2.2.13.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ Django 2.2.13 release notes
66

77
Django 2.2.13 fixes two security issues and a regression in 2.2.12.
88

9+
CVE-2020-13254: Potential data leakage via malformed memcached keys
10+
===================================================================
11+
12+
In cases where a memcached backend does not perform key validation, passing
13+
malformed cache keys could result in a key collision, and potential data
14+
leakage. In order to avoid this vulnerability, key validation is added to the
15+
memcached cache backends.
16+
917
CVE-2020-13596: Possible XSS via admin ``ForeignKeyRawIdWidget``
1018
================================================================
1119

tests/cache/tests.py

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from django.conf import settings
1616
from django.core import management, signals
1717
from django.core.cache import (
18-
DEFAULT_CACHE_ALIAS, CacheKeyWarning, cache, caches,
18+
DEFAULT_CACHE_ALIAS, CacheKeyWarning, InvalidCacheKey, cache, caches,
1919
)
2020
from django.core.cache.utils import make_template_fragment_key
2121
from django.db import close_old_connections, connection, connections
@@ -605,10 +605,10 @@ def test_zero_cull(self):
605605

606606
def _perform_invalid_key_test(self, key, expected_warning):
607607
"""
608-
All the builtin backends (except memcached, see below) should warn on
609-
keys that would be refused by memcached. This encourages portable
610-
caching code without making it too difficult to use production backends
611-
with more liberal key rules. Refs #6447.
608+
All the builtin backends should warn (except memcached that should
609+
error) on keys that would be refused by memcached. This encourages
610+
portable caching code without making it too difficult to use production
611+
backends with more liberal key rules. Refs #6447.
612612
"""
613613
# mimic custom ``make_key`` method being defined since the default will
614614
# never show the below warnings
@@ -1251,24 +1251,14 @@ def test_location_multiple_servers(self):
12511251
with self.settings(CACHES={'default': params}):
12521252
self.assertEqual(cache._servers, ['server1.tld', 'server2:11211'])
12531253

1254-
def test_invalid_key_characters(self):
1254+
def _perform_invalid_key_test(self, key, expected_warning):
12551255
"""
1256-
On memcached, we don't introduce a duplicate key validation
1257-
step (for speed reasons), we just let the memcached API
1258-
library raise its own exception on bad keys. Refs #6447.
1259-
1260-
In order to be memcached-API-library agnostic, we only assert
1261-
that a generic exception of some kind is raised.
1256+
Whilst other backends merely warn, memcached should raise for an
1257+
invalid key.
12621258
"""
1263-
# memcached does not allow whitespace or control characters in keys
1264-
# when using the ascii protocol.
1265-
with self.assertRaises(Exception):
1266-
cache.set('key with spaces', 'value')
1267-
1268-
def test_invalid_key_length(self):
1269-
# memcached limits key length to 250
1270-
with self.assertRaises(Exception):
1271-
cache.set('a' * 251, 'value')
1259+
msg = expected_warning.replace(key, ':1:%s' % key)
1260+
with self.assertRaisesMessage(InvalidCacheKey, msg):
1261+
cache.set(key, 'value')
12721262

12731263
def test_default_never_expiring_timeout(self):
12741264
# Regression test for #22845
@@ -1377,15 +1367,6 @@ class PyLibMCCacheTests(BaseMemcachedTests, TestCase):
13771367
# libmemcached manages its own connections.
13781368
should_disconnect_on_close = False
13791369

1380-
# By default, pylibmc/libmemcached don't verify keys client-side and so
1381-
# this test triggers a server-side bug that causes later tests to fail
1382-
# (#19914). The `verify_keys` behavior option could be set to True (which
1383-
# would avoid triggering the server-side bug), however this test would
1384-
# still fail due to https://github.com/lericson/pylibmc/issues/219.
1385-
@unittest.skip("triggers a memcached-server bug, causing subsequent tests to fail")
1386-
def test_invalid_key_characters(self):
1387-
pass
1388-
13891370
@override_settings(CACHES=caches_setting_for_tests(
13901371
base=PyLibMCCache_params,
13911372
exclude=memcached_excluded_caches,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy