!134 Fix CVE-2024-38875 CVE-2024-39329 CVE-2024-39330 CVE-2024-39614 CVE-2024-41989 CVE-2024-41990 CVE-2024-41991 CVE-2024-42005 CVE-2024-45230 CVE-2024-45231
From: @wk333 Reviewed-by: @cherry530 Signed-off-by: @cherry530
This commit is contained in:
commit
8553f6f573
154
CVE-2024-38875.patch
Normal file
154
CVE-2024-38875.patch
Normal file
@ -0,0 +1,154 @@
|
||||
From 8623260fb0949d368376a128bee2189ec0a67ae5 Mon Sep 17 00:00:00 2001
|
||||
From: nkrapp <nico.krapp@suse.com>
|
||||
Date: Mon, 22 Jul 2024 09:43:08 +0200
|
||||
Subject: [PATCH] [PATCH] [4.2.x] Fixed CVE-2024-38875 -- Mitigated potential
|
||||
DoS in urlize and urlizetrunc template filters.
|
||||
|
||||
---
|
||||
django/utils/html.py | 72 +++++++++++++++++++++++++++++-----
|
||||
tests/utils_tests/test_html.py | 21 ++++++----
|
||||
2 files changed, 75 insertions(+), 17 deletions(-)
|
||||
|
||||
diff --git a/django/utils/html.py b/django/utils/html.py
|
||||
index 7a33d5f68d..1dbe39ccd1 100644
|
||||
--- a/django/utils/html.py
|
||||
+++ b/django/utils/html.py
|
||||
@@ -1,5 +1,6 @@
|
||||
"""HTML utilities suitable for global use."""
|
||||
|
||||
+import html
|
||||
import json
|
||||
import re
|
||||
from html.parser import HTMLParser
|
||||
@@ -235,6 +235,16 @@ def smart_urlquote(url):
|
||||
return urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
+class CountsDict(dict):
|
||||
+ def __init__(self, *args, word, **kwargs):
|
||||
+ super().__init__(*args, *kwargs)
|
||||
+ self.word = word
|
||||
+
|
||||
+ def __missing__(self, key):
|
||||
+ self[key] = self.word.count(key)
|
||||
+ return self[key]
|
||||
+
|
||||
+
|
||||
@keep_lazy_text
|
||||
def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
"""
|
||||
@@ -259,6 +269,15 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
return x
|
||||
return '%s…' % x[:max(0, limit - 1)]
|
||||
|
||||
+ def wrapping_punctuation_openings():
|
||||
+ return "".join(dict(WRAPPING_PUNCTUATION).keys())
|
||||
+
|
||||
+ def trailing_punctuation_chars_no_semicolon():
|
||||
+ return TRAILING_PUNCTUATION_CHARS.replace(";", "")
|
||||
+
|
||||
+ def trailing_punctuation_chars_has_semicolon():
|
||||
+ return ";" in TRAILING_PUNCTUATION_CHARS
|
||||
+
|
||||
def unescape(text):
|
||||
"""
|
||||
If input URL is HTML-escaped, unescape it so that it can be safely fed
|
||||
@@ -273,21 +292,53 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
|
||||
Trim trailing and wrapping punctuation from `middle`. Return the items
|
||||
of the new state.
|
||||
"""
|
||||
+ # Strip all opening wrapping punctuation.
|
||||
+ middle = word.lstrip(wrapping_punctuation_openings())
|
||||
+ lead = word[: len(word) - len(middle)]
|
||||
+ trail = ""
|
||||
+
|
||||
# Continue trimming until middle remains unchanged.
|
||||
trimmed_something = True
|
||||
- while trimmed_something:
|
||||
+ counts = CountsDict(word=middle)
|
||||
+ while trimmed_something and middle:
|
||||
trimmed_something = False
|
||||
# Trim wrapping punctuation.
|
||||
for opening, closing in WRAPPING_PUNCTUATION:
|
||||
- if middle.startswith(opening):
|
||||
- middle = middle[len(opening):]
|
||||
- lead += opening
|
||||
- trimmed_something = True
|
||||
- # Keep parentheses at the end only if they're balanced.
|
||||
- if (middle.endswith(closing) and
|
||||
- middle.count(closing) == middle.count(opening) + 1):
|
||||
- middle = middle[:-len(closing)]
|
||||
- trail = closing + trail
|
||||
+ if counts[opening] < counts[closing]:
|
||||
+ rstripped = middle.rstrip(closing)
|
||||
+ if rstripped != middle:
|
||||
+ strip = counts[closing] - counts[opening]
|
||||
+ trail = middle[-strip:]
|
||||
+ middle = middle[:-strip]
|
||||
+ trimmed_something = True
|
||||
+ counts[closing] -= strip
|
||||
+
|
||||
+ rstripped = middle.rstrip(trailing_punctuation_chars_no_semicolon())
|
||||
+ if rstripped != middle:
|
||||
+ trail = middle[len(rstripped) :] + trail
|
||||
+ middle = rstripped
|
||||
+ trimmed_something = True
|
||||
+
|
||||
+ if trailing_punctuation_chars_has_semicolon() and middle.endswith(";"):
|
||||
+ # Only strip if not part of an HTML entity.
|
||||
+ amp = middle.rfind("&")
|
||||
+ if amp == -1:
|
||||
+ can_strip = True
|
||||
+ else:
|
||||
+ potential_entity = middle[amp:]
|
||||
+ escaped = html.unescape(potential_entity)
|
||||
+ can_strip = (escaped == potential_entity) or escaped.endswith(";")
|
||||
+
|
||||
+ if can_strip:
|
||||
+ rstripped = middle.rstrip(";")
|
||||
+ amount_stripped = len(middle) - len(rstripped)
|
||||
+ if amp > -1 and amount_stripped > 1:
|
||||
+ # Leave a trailing semicolon as might be an entity.
|
||||
+ trail = middle[len(rstripped) + 1 :] + trail
|
||||
+ middle = rstripped + ";"
|
||||
+ else:
|
||||
+ trail = middle[len(rstripped) :] + trail
|
||||
+ middle = rstripped
|
||||
trimmed_something = True
|
||||
# Trim trailing punctuation (after trimming wrapping punctuation,
|
||||
# as encoded entities contain ';'). Unescape entites to avoid
|
||||
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
|
||||
index 5cc2d9b95d..dc89009b63 100644
|
||||
--- a/tests/utils_tests/test_html.py
|
||||
+++ b/tests/utils_tests/test_html.py
|
||||
@@ -260,13 +260,20 @@ class TestUtilsHtml(SimpleTestCase):
|
||||
|
||||
def test_urlize_unchanged_inputs(self):
|
||||
tests = (
|
||||
- ('a' + '@a' * 50000) + 'a', # simple_email_re catastrophic test
|
||||
- ('a' + '.' * 1000000) + 'a', # trailing_punctuation catastrophic test
|
||||
- 'foo@',
|
||||
- '@foo.com',
|
||||
- 'foo@.example.com',
|
||||
- 'foo@localhost',
|
||||
- 'foo@localhost.',
|
||||
+ ("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test
|
||||
+ ("a" + "." * 1000000) + "a", # trailing_punctuation catastrophic test
|
||||
+ "foo@",
|
||||
+ "@foo.com",
|
||||
+ "foo@.example.com",
|
||||
+ "foo@localhost",
|
||||
+ "foo@localhost.",
|
||||
+ # trim_punctuation catastrophic tests
|
||||
+ "(" * 100_000 + ":" + ")" * 100_000,
|
||||
+ "(" * 100_000 + "&:" + ")" * 100_000,
|
||||
+ "([" * 100_000 + ":" + "])" * 100_000,
|
||||
+ "[(" * 100_000 + ":" + ")]" * 100_000,
|
||||
+ "([[" * 100_000 + ":" + "]])" * 100_000,
|
||||
+ "&:" + ";" * 100_000,
|
||||
)
|
||||
for value in tests:
|
||||
with self.subTest(value=value):
|
||||
--
|
||||
2.45.2
|
||||
|
||||
77
CVE-2024-39329.patch
Normal file
77
CVE-2024-39329.patch
Normal file
@ -0,0 +1,77 @@
|
||||
From c676b05c98df58252509dd5ad16b959c351ac770 Mon Sep 17 00:00:00 2001
|
||||
From: nkrapp <nico.krapp@suse.com>
|
||||
Date: Fri, 26 Jul 2024 14:01:36 +0200
|
||||
Subject: [PATCH] Fixed CVE-2024-39329 -- Standarized timing of
|
||||
verify_password() when checking unusuable passwords.
|
||||
|
||||
Refs #20760.
|
||||
|
||||
Thanks Michael Manfre for the fix and to Adam Johnson for the review.
|
||||
---
|
||||
django/contrib/auth/hashers.py | 10 ++++++++--
|
||||
tests/auth_tests/test_hashers.py | 22 ++++++++++++++++++++++
|
||||
2 files changed, 30 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
|
||||
index 1e8d7547fc..af55e57389 100644
|
||||
--- a/django/contrib/auth/hashers.py
|
||||
+++ b/django/contrib/auth/hashers.py
|
||||
@@ -36,14 +36,20 @@ def check_password(password, encoded, setter=None, preferred='default'):
|
||||
If setter is specified, it'll be called when you need to
|
||||
regenerate the password.
|
||||
"""
|
||||
- if password is None or not is_password_usable(encoded):
|
||||
- return False
|
||||
+ fake_runtime = password is None or not is_password_usable(encoded)
|
||||
|
||||
preferred = get_hasher(preferred)
|
||||
try:
|
||||
hasher = identify_hasher(encoded)
|
||||
except ValueError:
|
||||
# encoded is gibberish or uses a hasher that's no longer installed.
|
||||
+ fake_runtime = True
|
||||
+
|
||||
+ if fake_runtime:
|
||||
+ # Run the default password hasher once to reduce the timing difference
|
||||
+ # between an existing user with an unusable password and a nonexistent
|
||||
+ # user or missing hasher (similar to #20760).
|
||||
+ make_password(get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH))
|
||||
return False
|
||||
|
||||
hasher_changed = hasher.algorithm != preferred.algorithm
|
||||
diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py
|
||||
index ee6441b237..170bd07c4b 100644
|
||||
--- a/tests/auth_tests/test_hashers.py
|
||||
+++ b/tests/auth_tests/test_hashers.py
|
||||
@@ -433,6 +433,28 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||
check_password('wrong_password', encoded)
|
||||
self.assertEqual(hasher.harden_runtime.call_count, 1)
|
||||
|
||||
+ def test_check_password_calls_make_password_to_fake_runtime(self):
|
||||
+ hasher = get_hasher("default")
|
||||
+ cases = [
|
||||
+ (None, None, None), # no plain text password provided
|
||||
+ ("foo", make_password(password=None), None), # unusable encoded
|
||||
+ ("letmein", make_password(password="letmein"), ValueError), # valid encoded
|
||||
+ ]
|
||||
+ for password, encoded, hasher_side_effect in cases:
|
||||
+ with self.subTest(encoded=encoded):
|
||||
+ with mock.patch("django.contrib.auth.hashers.identify_hasher", side_effect=hasher_side_effect) as mock_identify_hasher:
|
||||
+ with mock.patch("django.contrib.auth.hashers.make_password") as mock_make_password:
|
||||
+ with mock.patch("django.contrib.auth.hashers.get_random_string", side_effect=lambda size: "x" * size):
|
||||
+ with mock.patch.object(hasher, "verify"):
|
||||
+ # Ensure make_password is called to standardize timing.
|
||||
+ check_password(password, encoded)
|
||||
+ self.assertEqual(hasher.verify.call_count, 0)
|
||||
+ self.assertEqual(mock_identify_hasher.mock_calls, [mock.call(encoded)])
|
||||
+ self.assertEqual(
|
||||
+ mock_make_password.mock_calls,
|
||||
+ [mock.call("x" * UNUSABLE_PASSWORD_SUFFIX_LENGTH)],
|
||||
+ )
|
||||
+
|
||||
|
||||
class BasePasswordHasherTests(SimpleTestCase):
|
||||
not_implemented_msg = 'subclasses of BasePasswordHasher must provide %s() method'
|
||||
--
|
||||
2.45.2
|
||||
|
||||
147
CVE-2024-39330.patch
Normal file
147
CVE-2024-39330.patch
Normal file
@ -0,0 +1,147 @@
|
||||
From 72af4b325aa9ffd96b18ef68d26ec2260e982c2a Mon Sep 17 00:00:00 2001
|
||||
From: nkrapp <nico.krapp@suse.com>
|
||||
Date: Wed, 24 Jul 2024 17:08:23 +0200
|
||||
Subject: [PATCH] Fixed CVE-2024-39330 -- Added extra file name validation in
|
||||
Storage's save method.
|
||||
|
||||
Thanks to Josh Schneier for the report, and to Carlton Gibson and Sarah
|
||||
Boyce for the reviews.
|
||||
---
|
||||
django/core/files/storage.py | 11 ++++++
|
||||
django/core/files/utils.py | 7 ++--
|
||||
tests/file_storage/test_base.py | 64 +++++++++++++++++++++++++++++++++
|
||||
tests/file_storage/tests.py | 6 ----
|
||||
4 files changed, 78 insertions(+), 10 deletions(-)
|
||||
create mode 100644 tests/file_storage/test_base.py
|
||||
|
||||
diff --git a/django/core/files/storage.py b/django/core/files/storage.py
|
||||
index ea5bbc82d0..8c633ec040 100644
|
||||
--- a/django/core/files/storage.py
|
||||
+++ b/django/core/files/storage.py
|
||||
@@ -50,7 +50,18 @@ class Storage:
|
||||
if not hasattr(content, 'chunks'):
|
||||
content = File(content, name)
|
||||
|
||||
+ # Ensure that the name is valid, before and after having the storage
|
||||
+ # system potentially modifying the name. This duplicates the check made
|
||||
+ # inside `get_available_name` but it's necessary for those cases where
|
||||
+ # `get_available_name` is overriden and validation is lost.
|
||||
+ validate_file_name(name, allow_relative_path=True)
|
||||
+
|
||||
+ # Potentially find a different name depending on storage constraints.
|
||||
name = self.get_available_name(name, max_length=max_length)
|
||||
+ # Validate the (potentially) new name.
|
||||
+ validate_file_name(name, allow_relative_path=True)
|
||||
+
|
||||
+ # The save operation should return the actual name of the file saved.
|
||||
name = self._save(name, content)
|
||||
# Ensure that the name returned from the storage system is still valid.
|
||||
validate_file_name(name, allow_relative_path=True)
|
||||
diff --git a/django/core/files/utils.py b/django/core/files/utils.py
|
||||
index f28cea1077..a1fea44ded 100644
|
||||
--- a/django/core/files/utils.py
|
||||
+++ b/django/core/files/utils.py
|
||||
@@ -10,10 +10,9 @@ def validate_file_name(name, allow_relative_path=False):
|
||||
raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
|
||||
|
||||
if allow_relative_path:
|
||||
- # Use PurePosixPath() because this branch is checked only in
|
||||
- # FileField.generate_filename() where all file paths are expected to be
|
||||
- # Unix style (with forward slashes).
|
||||
- path = pathlib.PurePosixPath(name)
|
||||
+ # Ensure that name can be treated as a pure posix path, i.e. Unix
|
||||
+ # style (with forward slashes).
|
||||
+ path = pathlib.PurePosixPath(str(name).replace("\\", "/"))
|
||||
if path.is_absolute() or '..' in path.parts:
|
||||
raise SuspiciousFileOperation(
|
||||
"Detected path traversal attempt in '%s'" % name
|
||||
diff --git a/tests/file_storage/test_base.py b/tests/file_storage/test_base.py
|
||||
new file mode 100644
|
||||
index 0000000000..7a0838f7a5
|
||||
--- /dev/null
|
||||
+++ b/tests/file_storage/test_base.py
|
||||
@@ -0,0 +1,64 @@
|
||||
+import os
|
||||
+from unittest import mock
|
||||
+
|
||||
+from django.core.exceptions import SuspiciousFileOperation
|
||||
+from django.core.files.storage import Storage
|
||||
+from django.test import SimpleTestCase
|
||||
+
|
||||
+
|
||||
+class CustomStorage(Storage):
|
||||
+ """Simple Storage subclass implementing the bare minimum for testing."""
|
||||
+
|
||||
+ def exists(self, name):
|
||||
+ return False
|
||||
+
|
||||
+ def _save(self, name):
|
||||
+ return name
|
||||
+
|
||||
+
|
||||
+class StorageValidateFileNameTests(SimpleTestCase):
|
||||
+ invalid_file_names = [
|
||||
+ os.path.join("path", "to", os.pardir, "test.file"),
|
||||
+ os.path.join(os.path.sep, "path", "to", "test.file"),
|
||||
+ ]
|
||||
+ error_msg = "Detected path traversal attempt in '%s'"
|
||||
+
|
||||
+ def test_validate_before_get_available_name(self):
|
||||
+ s = CustomStorage()
|
||||
+ # The initial name passed to `save` is not valid nor safe, fail early.
|
||||
+ for name in self.invalid_file_names:
|
||||
+ with self.subTest(name=name):
|
||||
+ with mock.patch.object(s, "get_available_name") as mock_get_available_names:
|
||||
+ with mock.patch.object(s, "_save") as mock_internal_save:
|
||||
+ with self.assertRaisesMessage(
|
||||
+ SuspiciousFileOperation, self.error_msg % name
|
||||
+ ):
|
||||
+ s.save(name, content="irrelevant")
|
||||
+ self.assertEqual(mock_get_available_names.mock_calls, [])
|
||||
+ self.assertEqual(mock_internal_save.mock_calls, [])
|
||||
+
|
||||
+ def test_validate_after_get_available_name(self):
|
||||
+ s = CustomStorage()
|
||||
+ # The initial name passed to `save` is valid and safe, but the returned
|
||||
+ # name from `get_available_name` is not.
|
||||
+ for name in self.invalid_file_names:
|
||||
+ with self.subTest(name=name):
|
||||
+ with mock.patch.object(s, "get_available_name", return_value=name):
|
||||
+ with mock.patch.object(s, "_save") as mock_internal_save:
|
||||
+ with self.assertRaisesMessage(
|
||||
+ SuspiciousFileOperation, self.error_msg % name
|
||||
+ ):
|
||||
+ s.save("valid-file-name.txt", content="irrelevant")
|
||||
+ self.assertEqual(mock_internal_save.mock_calls, [])
|
||||
+
|
||||
+ def test_validate_after_internal_save(self):
|
||||
+ s = CustomStorage()
|
||||
+ # The initial name passed to `save` is valid and safe, but the result
|
||||
+ # from `_save` is not (this is achieved by monkeypatching _save).
|
||||
+ for name in self.invalid_file_names:
|
||||
+ with self.subTest(name=name):
|
||||
+ with mock.patch.object(s, "_save", return_value=name):
|
||||
+ with self.assertRaisesMessage(
|
||||
+ SuspiciousFileOperation, self.error_msg % name
|
||||
+ ):
|
||||
+ s.save("valid-file-name.txt", content="irrelevant")
|
||||
diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py
|
||||
index 4c6f6920ed..0e692644b7 100644
|
||||
--- a/tests/file_storage/tests.py
|
||||
+++ b/tests/file_storage/tests.py
|
||||
@@ -291,12 +291,6 @@ class FileStorageTests(SimpleTestCase):
|
||||
|
||||
self.storage.delete('path/to/test.file')
|
||||
|
||||
- def test_file_save_abs_path(self):
|
||||
- test_name = 'path/to/test.file'
|
||||
- f = ContentFile('file saved with path')
|
||||
- f_name = self.storage.save(os.path.join(self.temp_dir, test_name), f)
|
||||
- self.assertEqual(f_name, test_name)
|
||||
-
|
||||
def test_save_doesnt_close(self):
|
||||
with TemporaryUploadedFile('test', 'text/plain', 1, 'utf8') as file:
|
||||
file.write(b'1')
|
||||
--
|
||||
2.45.2
|
||||
|
||||
195
CVE-2024-39614.patch
Normal file
195
CVE-2024-39614.patch
Normal file
@ -0,0 +1,195 @@
|
||||
From 2f128b1865bc43f6cf3583b1255bf1bd8be29e57 Mon Sep 17 00:00:00 2001
|
||||
From: nkrapp <nico.krapp@suse.com>
|
||||
Date: Mon, 22 Jul 2024 11:23:29 +0200
|
||||
Subject: [PATCH] Fixed CVE-2024-39614 -- Mitigated potential DoS in
|
||||
get_supported_language_variant().
|
||||
|
||||
Language codes are now parsed with a maximum length limit of 500 chars.
|
||||
|
||||
Thanks to MProgrammer for the report.
|
||||
---
|
||||
django/utils/translation/trans_real.py | 29 ++++++++++---
|
||||
docs/ref/utils.txt | 25 +++++++++++
|
||||
tests/i18n/tests.py | 59 ++++++++++++++++++++++++++
|
||||
3 files changed, 107 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
|
||||
index ecd701f3d8..0a237a5afc 100644
|
||||
--- a/django/utils/translation/trans_real.py
|
||||
+++ b/django/utils/translation/trans_real.py
|
||||
@@ -30,9 +30,10 @@ _default = None
|
||||
CONTEXT_SEPARATOR = "\x04"
|
||||
|
||||
# Maximum number of characters that will be parsed from the Accept-Language
|
||||
-# header to prevent possible denial of service or memory exhaustion attacks.
|
||||
-# About 10x longer than the longest value shown on MDN’s Accept-Language page.
|
||||
-ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500
|
||||
+# header or cookie to prevent possible denial of service or memory exhaustion
|
||||
+# attacks. About 10x longer than the longest value shown on MDN’s
|
||||
+# Accept-Language page.
|
||||
+LANGUAGE_CODE_MAX_LENGTH = 500
|
||||
|
||||
# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
|
||||
# and RFC 3066, section 2.1
|
||||
@@ -473,12 +474,28 @@ def get_supported_language_variant(lang_code, strict=False):
|
||||
If `strict` is False (the default), look for a country-specific variant
|
||||
when neither the language code nor its generic variant is found.
|
||||
|
||||
+ The language code is truncated to a maximum length to avoid potential
|
||||
+ denial of service attacks.
|
||||
+
|
||||
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
|
||||
as the provided language codes are taken from the HTTP request. See also
|
||||
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
|
||||
"""
|
||||
if lang_code:
|
||||
- # If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
|
||||
+ # Truncate the language code to a maximum length to avoid potential
|
||||
+ # denial of service attacks.
|
||||
+ if len(lang_code) > LANGUAGE_CODE_MAX_LENGTH:
|
||||
+ index = lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)
|
||||
+ if (
|
||||
+ not strict
|
||||
+ and index > 0
|
||||
+ ):
|
||||
+ # There is a generic variant under the maximum length accepted length.
|
||||
+ lang_code = lang_code[:index]
|
||||
+ else:
|
||||
+ raise ValueError("'lang_code' exceeds the maximum accepted length")
|
||||
+ # If 'zh-hant-tw' is not supported, try special fallback or subsequent
|
||||
+ # language codes i.e. 'zh-hant' and 'zh'.
|
||||
possible_lang_codes = [lang_code]
|
||||
try:
|
||||
possible_lang_codes.extend(LANG_INFO[lang_code]['fallback'])
|
||||
@@ -599,13 +616,13 @@ def parse_accept_lang_header(lang_string):
|
||||
functools.lru_cache() to avoid repetitive parsing of common header values.
|
||||
"""
|
||||
# If the header value doesn't exceed the maximum allowed length, parse it.
|
||||
- if len(lang_string) <= ACCEPT_LANGUAGE_HEADER_MAX_LENGTH:
|
||||
+ if len(lang_string) <= LANGUAGE_CODE_MAX_LENGTH:
|
||||
return _parse_accept_lang_header(lang_string)
|
||||
|
||||
# If there is at least one comma in the value, parse up to the last comma
|
||||
# before the max length, skipping any truncated parts at the end of the
|
||||
# header value.
|
||||
- index = lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)
|
||||
+ index = lang_string.rfind(",", 0, LANGUAGE_CODE_MAX_LENGTH)
|
||||
if index > 0:
|
||||
return _parse_accept_lang_header(lang_string[:index])
|
||||
|
||||
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
|
||||
index 390f167ce2..d0a8e8c1f3 100644
|
||||
--- a/docs/ref/utils.txt
|
||||
+++ b/docs/ref/utils.txt
|
||||
@@ -1150,6 +1150,31 @@ functions without the ``u``.
|
||||
|
||||
Raises :exc:`LookupError` if nothing is found.
|
||||
|
||||
+.. function:: get_supported_language_variant(lang_code, strict=False)
|
||||
+
|
||||
+ Returns ``lang_code`` if it's in the :setting:`LANGUAGES` setting, possibly
|
||||
+ selecting a more generic variant. For example, ``'es'`` is returned if
|
||||
+ ``lang_code`` is ``'es-ar'`` and ``'es'`` is in :setting:`LANGUAGES` but
|
||||
+ ``'es-ar'`` isn't.
|
||||
+
|
||||
+ ``lang_code`` has a maximum accepted length of 500 characters. A
|
||||
+ :exc:`ValueError` is raised if ``lang_code`` exceeds this limit and
|
||||
+ ``strict`` is ``True``, or if there is no generic variant and ``strict``
|
||||
+ is ``False``.
|
||||
+
|
||||
+ If ``strict`` is ``False`` (the default), a country-specific variant may
|
||||
+ be returned when neither the language code nor its generic variant is found.
|
||||
+ For example, if only ``'es-co'`` is in :setting:`LANGUAGES`, that's
|
||||
+ returned for ``lang_code``\s like ``'es'`` and ``'es-ar'``. Those matches
|
||||
+ aren't returned if ``strict=True``.
|
||||
+
|
||||
+ Raises :exc:`LookupError` if nothing is found.
|
||||
+
|
||||
+ .. versionchanged:: 4.2.14
|
||||
+
|
||||
+ In older versions, ``lang_code`` values over 500 characters were
|
||||
+ processed without raising a :exc:`ValueError`.
|
||||
+
|
||||
.. function:: to_locale(language)
|
||||
|
||||
Turns a language name (en-us) into a locale name (en_US).
|
||||
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
|
||||
index 6efc3a5ae3..3087e5b6a6 100644
|
||||
--- a/tests/i18n/tests.py
|
||||
+++ b/tests/i18n/tests.py
|
||||
@@ -39,6 +39,7 @@ from django.utils.translation import (
|
||||
from django.utils.translation.reloader import (
|
||||
translation_file_changed, watch_for_translation_changes,
|
||||
)
|
||||
+from django.utils.translation.trans_real import LANGUAGE_CODE_MAX_LENGTH
|
||||
|
||||
from .forms import CompanyForm, I18nForm, SelectDateForm
|
||||
from .models import Company, TestModel
|
||||
@@ -1434,6 +1435,64 @@ class MiscTests(SimpleTestCase):
|
||||
r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'zh-hans'}
|
||||
r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'}
|
||||
self.assertEqual(g(r), 'zh-hans')
|
||||
+
|
||||
+ @override_settings(
|
||||
+ USE_I18N=True,
|
||||
+ LANGUAGES=[
|
||||
+ ("en", "English"),
|
||||
+ ("ar-dz", "Algerian Arabic"),
|
||||
+ ("de", "German"),
|
||||
+ ("de-at", "Austrian German"),
|
||||
+ ("pt-BR", "Portuguese (Brazil)"),
|
||||
+ ],
|
||||
+ )
|
||||
+ def test_get_supported_language_variant_real(self):
|
||||
+ g = trans_real.get_supported_language_variant
|
||||
+ self.assertEqual(g("en"), "en")
|
||||
+ self.assertEqual(g("en-gb"), "en")
|
||||
+ self.assertEqual(g("de"), "de")
|
||||
+ self.assertEqual(g("de-at"), "de-at")
|
||||
+ self.assertEqual(g("de-ch"), "de")
|
||||
+ self.assertEqual(g("pt-br"), "pt-br")
|
||||
+ self.assertEqual(g("pt-BR"), "pt-BR")
|
||||
+ self.assertEqual(g("pt"), "pt-br")
|
||||
+ self.assertEqual(g("pt-pt"), "pt-br")
|
||||
+ self.assertEqual(g("ar-dz"), "ar-dz")
|
||||
+ self.assertEqual(g("ar-DZ"), "ar-DZ")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("pt", strict=True)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("pt-pt", strict=True)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("xyz")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("xy-zz")
|
||||
+ msg = "'lang_code' exceeds the maximum accepted length"
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("x" * LANGUAGE_CODE_MAX_LENGTH)
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ g("x" * (LANGUAGE_CODE_MAX_LENGTH + 1))
|
||||
+ # 167 * 3 = 501 which is LANGUAGE_CODE_MAX_LENGTH + 1.
|
||||
+ self.assertEqual(g("en-" * 167), "en")
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ g("en-" * 167, strict=True)
|
||||
+ self.assertEqual(g("en-" * 30000), "en") # catastrophic test
|
||||
+
|
||||
+ def test_get_supported_language_variant_null(self):
|
||||
+ g = trans_null.get_supported_language_variant
|
||||
+ self.assertEqual(g(settings.LANGUAGE_CODE), settings.LANGUAGE_CODE)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("pt")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("de")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("de-at")
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("de", strict=True)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("de-at", strict=True)
|
||||
+ with self.assertRaises(LookupError):
|
||||
+ g("xyz")
|
||||
|
||||
@override_settings(
|
||||
USE_I18N=True,
|
||||
--
|
||||
2.45.2
|
||||
|
||||
76
CVE-2024-41989.patch
Normal file
76
CVE-2024-41989.patch
Normal file
@ -0,0 +1,76 @@
|
||||
From 0521744d21a7854e849336af1e3a3aad44cee017 Mon Sep 17 00:00:00 2001
|
||||
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
||||
Date: Fri, 12 Jul 2024 11:38:34 +0200
|
||||
Subject: [PATCH 1/4] [4.2.x] Fixed CVE-2024-41989 -- Prevented excessive
|
||||
memory consumption in floatformat.
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Thanks Elias Myllymäki for the report.
|
||||
|
||||
Co-authored-by: Shai Berger <shai@platonix.com>
|
||||
---
|
||||
django/template/defaultfilters.py | 13 +++++++++++++
|
||||
.../filter_tests/test_floatformat.py | 17 +++++++++++++++++
|
||||
3 files changed, 39 insertions(+)
|
||||
|
||||
Index: Django-2.2.28/django/template/defaultfilters.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/template/defaultfilters.py
|
||||
+++ Django-2.2.28/django/template/defaultfilters.py
|
||||
@@ -135,6 +135,19 @@ def floatformat(text, arg=-1):
|
||||
except ValueError:
|
||||
return input_val
|
||||
|
||||
+ _, digits, exponent = d.as_tuple()
|
||||
+ try:
|
||||
+ number_of_digits_and_exponent_sum = len(digits) + abs(exponent)
|
||||
+ except TypeError:
|
||||
+ # Exponent values can be "F", "n", "N".
|
||||
+ number_of_digits_and_exponent_sum = 0
|
||||
+
|
||||
+ # Values with more than 200 digits, or with a large exponent, are returned "as is"
|
||||
+ # to avoid high memory consumption and potential denial-of-service attacks.
|
||||
+ # The cut-off of 200 is consistent with django.utils.numberformat.floatformat().
|
||||
+ if number_of_digits_and_exponent_sum > 200:
|
||||
+ return input_val
|
||||
+
|
||||
try:
|
||||
m = int(d) - d
|
||||
except (ValueError, OverflowError, InvalidOperation):
|
||||
Index: Django-2.2.28/tests/template_tests/filter_tests/test_floatformat.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/template_tests/filter_tests/test_floatformat.py
|
||||
+++ Django-2.2.28/tests/template_tests/filter_tests/test_floatformat.py
|
||||
@@ -55,6 +55,7 @@ class FunctionTests(SimpleTestCase):
|
||||
self.assertEqual(floatformat(1.5e-15, 20), '0.00000000000000150000')
|
||||
self.assertEqual(floatformat(1.5e-15, -20), '0.00000000000000150000')
|
||||
self.assertEqual(floatformat(1.00000000000000015, 16), '1.0000000000000002')
|
||||
+ self.assertEqual(floatformat("1e199"), "1" + "0" * 199)
|
||||
|
||||
def test_zero_values(self):
|
||||
self.assertEqual(floatformat(0, 6), '0.000000')
|
||||
@@ -68,6 +69,22 @@ class FunctionTests(SimpleTestCase):
|
||||
self.assertEqual(floatformat(pos_inf), 'inf')
|
||||
self.assertEqual(floatformat(neg_inf), '-inf')
|
||||
self.assertEqual(floatformat(pos_inf / pos_inf), 'nan')
|
||||
+ self.assertEqual(floatformat("inf"), "inf")
|
||||
+ self.assertEqual(floatformat("NaN"), "NaN")
|
||||
+
|
||||
+ def test_too_many_digits_to_render(self):
|
||||
+ cases = [
|
||||
+ "1e200",
|
||||
+ "1E200",
|
||||
+ "1E10000000000000000",
|
||||
+ "-1E10000000000000000",
|
||||
+ "1e10000000000000000",
|
||||
+ "-1e10000000000000000",
|
||||
+ "1" + "0" * 1_000_000,
|
||||
+ ]
|
||||
+ for value in cases:
|
||||
+ with self.subTest(value=value):
|
||||
+ self.assertEqual(floatformat(value), value)
|
||||
|
||||
def test_float_dunder_method(self):
|
||||
class FloatWrapper:
|
||||
61
CVE-2024-41990.patch
Normal file
61
CVE-2024-41990.patch
Normal file
@ -0,0 +1,61 @@
|
||||
From 729d7934e34ff91f262f3e7089e32cab701b09ca Mon Sep 17 00:00:00 2001
|
||||
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
||||
Date: Thu, 18 Jul 2024 13:19:34 +0200
|
||||
Subject: [PATCH 2/4] [4.2.x] Fixed CVE-2024-41990 -- Mitigated potential DoS
|
||||
in urlize and urlizetrunc template filters.
|
||||
|
||||
Thanks to MProgrammer for the report.
|
||||
---
|
||||
django/utils/html.py | 18 ++++++++----------
|
||||
tests/utils_tests/test_html.py | 2 ++
|
||||
3 files changed, 17 insertions(+), 10 deletions(-)
|
||||
|
||||
Index: Django-2.2.28/django/utils/html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/utils/html.py
|
||||
+++ Django-2.2.28/django/utils/html.py
|
||||
@@ -314,7 +314,11 @@ def urlize(text, trim_url_limit=None, no
|
||||
trimmed_something = True
|
||||
counts[closing] -= strip
|
||||
|
||||
- rstripped = middle.rstrip(trailing_punctuation_chars_no_semicolon())
|
||||
+ amp = middle.rfind("&")
|
||||
+ if amp == -1:
|
||||
+ rstripped = middle.rstrip(TRAILING_PUNCTUATION_CHARS)
|
||||
+ else:
|
||||
+ rstripped = middle.rstrip(trailing_punctuation_chars_no_semicolon())
|
||||
if rstripped != middle:
|
||||
trail = middle[len(rstripped) :] + trail
|
||||
middle = rstripped
|
||||
@@ -322,15 +326,9 @@ def urlize(text, trim_url_limit=None, no
|
||||
|
||||
if trailing_punctuation_chars_has_semicolon() and middle.endswith(";"):
|
||||
# Only strip if not part of an HTML entity.
|
||||
- amp = middle.rfind("&")
|
||||
- if amp == -1:
|
||||
- can_strip = True
|
||||
- else:
|
||||
- potential_entity = middle[amp:]
|
||||
- escaped = html.unescape(potential_entity)
|
||||
- can_strip = (escaped == potential_entity) or escaped.endswith(";")
|
||||
-
|
||||
- if can_strip:
|
||||
+ potential_entity = middle[amp:]
|
||||
+ escaped = html.unescape(potential_entity)
|
||||
+ if escaped == potential_entity or escaped.endswith(";"):
|
||||
rstripped = middle.rstrip(";")
|
||||
amount_stripped = len(middle) - len(rstripped)
|
||||
if amp > -1 and amount_stripped > 1:
|
||||
Index: Django-2.2.28/tests/utils_tests/test_html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
|
||||
+++ Django-2.2.28/tests/utils_tests/test_html.py
|
||||
@@ -274,6 +274,8 @@ class TestUtilsHtml(SimpleTestCase):
|
||||
"[(" * 100_000 + ":" + ")]" * 100_000,
|
||||
"([[" * 100_000 + ":" + "]])" * 100_000,
|
||||
"&:" + ";" * 100_000,
|
||||
+ "&.;" * 100_000,
|
||||
+ ".;" * 100_000,
|
||||
)
|
||||
for value in tests:
|
||||
with self.subTest(value=value):
|
||||
98
CVE-2024-41991.patch
Normal file
98
CVE-2024-41991.patch
Normal file
@ -0,0 +1,98 @@
|
||||
From 772a73f70c3d249c99c23012849e66276b7b0715 Mon Sep 17 00:00:00 2001
|
||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
||||
Date: Wed, 10 Jul 2024 20:30:12 +0200
|
||||
Subject: [PATCH 3/4] [4.2.x] Fixed CVE-2024-41991 -- Prevented potential ReDoS
|
||||
in django.utils.html.urlize() and AdminURLFieldWidget.
|
||||
|
||||
Thanks Seokchan Yoon for the report.
|
||||
|
||||
Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
||||
---
|
||||
django/contrib/admin/widgets.py | 2 +-
|
||||
django/utils/html.py | 10 ++++++++--
|
||||
tests/admin_widgets/tests.py | 7 ++++++-
|
||||
tests/utils_tests/test_html.py | 13 +++++++++++++
|
||||
5 files changed, 35 insertions(+), 4 deletions(-)
|
||||
|
||||
Index: Django-2.2.28/django/contrib/admin/widgets.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/contrib/admin/widgets.py
|
||||
+++ Django-2.2.28/django/contrib/admin/widgets.py
|
||||
@@ -344,7 +344,7 @@ class AdminURLFieldWidget(forms.URLInput
|
||||
context = super().get_context(name, value, attrs)
|
||||
context['current_label'] = _('Currently:')
|
||||
context['change_label'] = _('Change:')
|
||||
- context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else ''
|
||||
+ context['widget']['href'] = smart_urlquote(context['widget']['value']) if url_valid else ''
|
||||
context['url_valid'] = url_valid
|
||||
return context
|
||||
|
||||
Index: Django-2.2.28/django/utils/html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/utils/html.py
|
||||
+++ Django-2.2.28/django/utils/html.py
|
||||
@@ -33,6 +33,8 @@ _html_escapes = {
|
||||
ord("'"): ''',
|
||||
}
|
||||
|
||||
+MAX_URL_LENGTH = 2048
|
||||
+
|
||||
|
||||
@keep_lazy(str, SafeText)
|
||||
def escape(text):
|
||||
@@ -360,6 +362,10 @@ def urlize(text, trim_url_limit=None, no
|
||||
except ValueError:
|
||||
# value contains more than one @.
|
||||
return False
|
||||
+ # Max length for domain name labels is 63 characters per RFC 1034.
|
||||
+ # Helps to avoid ReDoS vectors in the domain part.
|
||||
+ if len(p2) > 63:
|
||||
+ return False
|
||||
# Dot must be in p2 (e.g. example.com)
|
||||
if '.' not in p2 or p2.startswith('.'):
|
||||
return False
|
||||
@@ -378,9 +384,9 @@ def urlize(text, trim_url_limit=None, no
|
||||
# Make URL we want to point to.
|
||||
url = None
|
||||
nofollow_attr = ' rel="nofollow"' if nofollow else ''
|
||||
- if simple_url_re.match(middle):
|
||||
+ if len(middle) <= MAX_URL_LENGTH and simple_url_re.match(middle):
|
||||
url = smart_urlquote(unescape(middle))
|
||||
- elif simple_url_2_re.match(middle):
|
||||
+ elif len(middle) <= MAX_URL_LENGTH and simple_url_2_re.match(middle):
|
||||
url = smart_urlquote('http://%s' % unescape(middle))
|
||||
elif ':' not in middle and is_email_simple(middle):
|
||||
local, domain = middle.rsplit('@', 1)
|
||||
Index: Django-2.2.28/tests/admin_widgets/tests.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/admin_widgets/tests.py
|
||||
+++ Django-2.2.28/tests/admin_widgets/tests.py
|
||||
@@ -336,7 +336,12 @@ class AdminSplitDateTimeWidgetTest(Simpl
|
||||
class AdminURLWidgetTest(SimpleTestCase):
|
||||
def test_get_context_validates_url(self):
|
||||
w = widgets.AdminURLFieldWidget()
|
||||
- for invalid in ['', '/not/a/full/url/', 'javascript:alert("Danger XSS!")']:
|
||||
+ for invalid in [
|
||||
+ "",
|
||||
+ "/not/a/full/url/",
|
||||
+ 'javascript:alert("Danger XSS!")',
|
||||
+ "http://" + "한.글." * 1_000_000 + "com",
|
||||
+ ]:
|
||||
with self.subTest(url=invalid):
|
||||
self.assertFalse(w.get_context('name', invalid, {})['url_valid'])
|
||||
self.assertTrue(w.get_context('name', 'http://example.com', {})['url_valid'])
|
||||
Index: Django-2.2.28/tests/utils_tests/test_html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
|
||||
+++ Django-2.2.28/tests/utils_tests/test_html.py
|
||||
@@ -261,6 +261,10 @@ class TestUtilsHtml(SimpleTestCase):
|
||||
def test_urlize_unchanged_inputs(self):
|
||||
tests = (
|
||||
("a" + "@a" * 50000) + "a", # simple_email_re catastrophic test
|
||||
+ # Unicode domain catastrophic tests.
|
||||
+ "a@" + "한.글." * 1_000_000 + "a",
|
||||
+ "http://" + "한.글." * 1_000_000 + "com",
|
||||
+ "www." + "한.글." * 1_000_000 + "com",
|
||||
("a" + "." * 1000000) + "a", # trailing_punctuation catastrophic test
|
||||
"foo@",
|
||||
"@foo.com",
|
||||
84
CVE-2024-42005.patch
Normal file
84
CVE-2024-42005.patch
Normal file
@ -0,0 +1,84 @@
|
||||
From b6de28f897709ee5d94ca2da21bcc98f9dade01c Mon Sep 17 00:00:00 2001
|
||||
From: Simon Charette <charette.s@gmail.com>
|
||||
Date: Thu, 25 Jul 2024 18:19:13 +0200
|
||||
Subject: [PATCH 4/4] [4.2.x] Fixed CVE-2024-42005 -- Mitigated
|
||||
QuerySet.values() SQL injection attacks against JSON fields.
|
||||
|
||||
Thanks Eyal (eyalgabay) for the report.
|
||||
---
|
||||
django/db/models/sql/query.py | 2 ++
|
||||
tests/expressions/models.py | 7 +++++++
|
||||
tests/expressions/test_queryset_values.py | 17 +++++++++++++++--
|
||||
4 files changed, 31 insertions(+), 2 deletions(-)
|
||||
|
||||
Index: Django-2.0.7/django/db/models/sql/query.py
|
||||
===================================================================
|
||||
--- Django-2.0.7.orig/django/db/models/sql/query.py
|
||||
+++ Django-2.0.7/django/db/models/sql/query.py
|
||||
@@ -1924,6 +1924,8 @@ class Query:
|
||||
self.clear_select_fields()
|
||||
|
||||
if fields:
|
||||
+ for field in fields:
|
||||
+ self.check_alias(field)
|
||||
field_names = []
|
||||
extra_names = []
|
||||
annotation_names = []
|
||||
Index: Django-2.0.7/tests/expressions/models.py
|
||||
===================================================================
|
||||
--- Django-2.0.7.orig/tests/expressions/models.py
|
||||
+++ Django-2.0.7/tests/expressions/models.py
|
||||
@@ -4,6 +4,7 @@ Tests for F() query expression syntax.
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
+from django.contrib.postgres.fields import JSONField
|
||||
|
||||
|
||||
class Employee(models.Model):
|
||||
@@ -91,3 +92,10 @@ class UUID(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.uuid
|
||||
+
|
||||
+
|
||||
+class JSONFieldModel(models.Model):
|
||||
+ data = JSONField(null=True)
|
||||
+
|
||||
+ class Meta:
|
||||
+ required_db_features = {"supports_json_field"}
|
||||
Index: Django-2.0.7/tests/expressions/test_queryset_values.py
|
||||
===================================================================
|
||||
--- Django-2.0.7.orig/tests/expressions/test_queryset_values.py
|
||||
+++ Django-2.0.7/tests/expressions/test_queryset_values.py
|
||||
@@ -1,8 +1,8 @@
|
||||
from django.db.models.aggregates import Sum
|
||||
from django.db.models.expressions import F
|
||||
-from django.test import TestCase
|
||||
+from django.test import TestCase, skipUnlessDBFeature
|
||||
|
||||
-from .models import Company, Employee
|
||||
+from .models import Company, Employee, JSONFieldModel
|
||||
|
||||
|
||||
class ValuesExpressionsTests(TestCase):
|
||||
@@ -36,6 +36,19 @@ class ValuesExpressionsTests(TestCase):
|
||||
with self.assertRaisesMessage(ValueError, msg):
|
||||
Company.objects.values(**{crafted_alias: F("ceo__salary")})
|
||||
|
||||
+ @skipUnlessDBFeature("supports_json_field")
|
||||
+ def test_values_expression_alias_sql_injection_json_field(self):
|
||||
+ crafted_alias = """injected_name" from "expressions_company"; --"""
|
||||
+ msg = (
|
||||
+ "Column aliases cannot contain whitespace characters, quotation marks, "
|
||||
+ "semicolons, or SQL comments."
|
||||
+ )
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ JSONFieldModel.objects.values(f"data__{crafted_alias}")
|
||||
+
|
||||
+ with self.assertRaisesMessage(ValueError, msg):
|
||||
+ JSONFieldModel.objects.values_list(f"data__{crafted_alias}")
|
||||
+
|
||||
def test_values_expression_group_by(self):
|
||||
# values() applies annotate() first, so values selected are grouped by
|
||||
# id, not firstname.
|
||||
133
CVE-2024-45230.patch
Normal file
133
CVE-2024-45230.patch
Normal file
@ -0,0 +1,133 @@
|
||||
From 65a776dd25b657cc32edafaad98d91aa0b51e641 Mon Sep 17 00:00:00 2001
|
||||
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
||||
Date: Mon, 12 Aug 2024 15:17:57 +0200
|
||||
Subject: [PATCH 1/2] [4.2.x] Fixed CVE-2024-45230 -- Mitigated potential DoS
|
||||
in urlize and urlizetrunc template filters.
|
||||
|
||||
Thanks MProgrammer (https://hackerone.com/mprogrammer) for the report.
|
||||
---
|
||||
django/utils/html.py | 17 ++++++++------
|
||||
docs/ref/templates/builtins.txt | 11 ++++++++++
|
||||
docs/releases/4.2.16.txt | 15 +++++++++++++
|
||||
docs/releases/index.txt | 1 +
|
||||
.../filter_tests/test_urlize.py | 22 +++++++++++++++++++
|
||||
tests/utils_tests/test_html.py | 1 +
|
||||
6 files changed, 60 insertions(+), 7 deletions(-)
|
||||
create mode 100644 docs/releases/4.2.16.txt
|
||||
|
||||
Index: Django-2.2.28/django/utils/html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/utils/html.py
|
||||
+++ Django-2.2.28/django/utils/html.py
|
||||
@@ -322,14 +322,17 @@ def urlize(text, trim_url_limit=None, no
|
||||
potential_entity = middle[amp:]
|
||||
escaped = html.unescape(potential_entity)
|
||||
if escaped == potential_entity or escaped.endswith(";"):
|
||||
- rstripped = middle.rstrip(";")
|
||||
- amount_stripped = len(middle) - len(rstripped)
|
||||
- if amp > -1 and amount_stripped > 1:
|
||||
- # Leave a trailing semicolon as might be an entity.
|
||||
- trail = middle[len(rstripped) + 1 :] + trail
|
||||
- middle = rstripped + ";"
|
||||
+ rstripped = middle.rstrip(TRAILING_PUNCTUATION_CHARS)
|
||||
+ trail_start = len(rstripped)
|
||||
+ amount_trailing_semicolons = len(middle) - len(middle.rstrip(";"))
|
||||
+ if amp > -1 and amount_trailing_semicolons > 1:
|
||||
+ # Leave up to most recent semicolon as might be an entity.
|
||||
+ recent_semicolon = middle[trail_start:].index(";")
|
||||
+ middle_semicolon_index = recent_semicolon + trail_start + 1
|
||||
+ trail = middle[middle_semicolon_index:] + trail
|
||||
+ middle = rstripped + middle[trail_start:middle_semicolon_index]
|
||||
else:
|
||||
- trail = middle[len(rstripped) :] + trail
|
||||
+ trail = middle[trail_start:] + trail
|
||||
middle = rstripped
|
||||
trimmed_something = True
|
||||
# Trim trailing punctuation (after trimming wrapping punctuation,
|
||||
Index: Django-2.2.28/docs/ref/templates/builtins.txt
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/docs/ref/templates/builtins.txt
|
||||
+++ Django-2.2.28/docs/ref/templates/builtins.txt
|
||||
@@ -2463,6 +2463,17 @@ Django's built-in :tfilter:`escape` filt
|
||||
email addresses that contain single quotes (``'``), things won't work as
|
||||
expected. Apply this filter only to plain text.
|
||||
|
||||
+.. warning::
|
||||
+
|
||||
+ Using ``urlize`` or ``urlizetrunc`` can incur a performance penalty, which
|
||||
+ can become severe when applied to user controlled values such as content
|
||||
+ stored in a :class:`~django.db.models.TextField`. You can use
|
||||
+ :tfilter:`truncatechars` to add a limit to such inputs:
|
||||
+
|
||||
+ .. code-block:: html+django
|
||||
+
|
||||
+ {{ value|truncatechars:500|urlize }}
|
||||
+
|
||||
.. templatefilter:: urlizetrunc
|
||||
|
||||
``urlizetrunc``
|
||||
Index: Django-2.2.28/docs/releases/4.2.16.txt
|
||||
===================================================================
|
||||
--- /dev/null
|
||||
+++ Django-2.2.28/docs/releases/4.2.16.txt
|
||||
@@ -0,0 +1,15 @@
|
||||
+===========================
|
||||
+Django 4.2.16 release notes
|
||||
+===========================
|
||||
+
|
||||
+*September 3, 2024*
|
||||
+
|
||||
+Django 4.2.16 fixes one security issue with severity "moderate" and one
|
||||
+security issues with severity "low" in 4.2.15.
|
||||
+
|
||||
+CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html.urlize()``
|
||||
+===========================================================================================
|
||||
+
|
||||
+:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
|
||||
+denial-of-service attack via very large inputs with a specific sequence of
|
||||
+characters.
|
||||
Index: Django-2.2.28/tests/template_tests/filter_tests/test_urlize.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/template_tests/filter_tests/test_urlize.py
|
||||
+++ Django-2.2.28/tests/template_tests/filter_tests/test_urlize.py
|
||||
@@ -260,6 +260,28 @@ class FunctionTests(SimpleTestCase):
|
||||
'A test <a href="http://testing.com/example" rel="nofollow">http://testing.com/example</a>.,:;)"!'
|
||||
)
|
||||
|
||||
+ def test_trailing_semicolon(self):
|
||||
+ self.assertEqual(
|
||||
+ urlize("http://example.com?x=&", autoescape=False),
|
||||
+ '<a href="http://example.com?x=" rel="nofollow">'
|
||||
+ "http://example.com?x=&</a>",
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ urlize("http://example.com?x=&;", autoescape=False),
|
||||
+ '<a href="http://example.com?x=" rel="nofollow">'
|
||||
+ "http://example.com?x=&</a>;",
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ urlize("http://example.com?x=&;;", autoescape=False),
|
||||
+ '<a href="http://example.com?x=" rel="nofollow">'
|
||||
+ "http://example.com?x=&</a>;;",
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ urlize("http://example.com?x=&.;...;", autoescape=False),
|
||||
+ '<a href="http://example.com?x=" rel="nofollow">'
|
||||
+ "http://example.com?x=&</a>.;...;",
|
||||
+ )
|
||||
+
|
||||
def test_brackets(self):
|
||||
"""
|
||||
#19070 - Check urlize handles brackets properly
|
||||
Index: Django-2.2.28/tests/utils_tests/test_html.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/utils_tests/test_html.py
|
||||
+++ Django-2.2.28/tests/utils_tests/test_html.py
|
||||
@@ -282,6 +282,7 @@ class TestUtilsHtml(SimpleTestCase):
|
||||
"&:" + ";" * 100_000,
|
||||
"&.;" * 100_000,
|
||||
".;" * 100_000,
|
||||
+ "&" + ";:" * 100_000,
|
||||
)
|
||||
for value in tests:
|
||||
with self.subTest(value=value):
|
||||
133
CVE-2024-45231.patch
Normal file
133
CVE-2024-45231.patch
Normal file
@ -0,0 +1,133 @@
|
||||
From fe42da9cdacd9f43fb0d499244314c36f9a11a19 Mon Sep 17 00:00:00 2001
|
||||
From: Natalia <124304+nessita@users.noreply.github.com>
|
||||
Date: Mon, 19 Aug 2024 14:47:38 -0300
|
||||
Subject: [PATCH 2/2] [4.2.x] Fixed CVE-2024-45231 -- Avoided server error on
|
||||
password reset when email sending fails.
|
||||
|
||||
On successful submission of a password reset request, an email is sent
|
||||
to the accounts known to the system. If sending this email fails (due to
|
||||
email backend misconfiguration, service provider outage, network issues,
|
||||
etc.), an attacker might exploit this by detecting which password reset
|
||||
requests succeed and which ones generate a 500 error response.
|
||||
|
||||
Thanks to Thibaut Spriet for the report, and to Mariusz Felisiak and
|
||||
Sarah Boyce for the reviews.
|
||||
---
|
||||
django/contrib/auth/forms.py | 9 ++++++++-
|
||||
docs/ref/logging.txt | 12 ++++++++++++
|
||||
docs/releases/4.2.16.txt | 11 +++++++++++
|
||||
docs/topics/auth/default.txt | 4 +++-
|
||||
tests/auth_tests/test_forms.py | 21 +++++++++++++++++++++
|
||||
tests/mail/custombackend.py | 5 +++++
|
||||
6 files changed, 60 insertions(+), 2 deletions(-)
|
||||
|
||||
Index: Django-2.2.28/django/contrib/auth/forms.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/django/contrib/auth/forms.py
|
||||
+++ Django-2.2.28/django/contrib/auth/forms.py
|
||||
@@ -1,3 +1,4 @@
|
||||
+import logging
|
||||
import unicodedata
|
||||
|
||||
from django import forms
|
||||
@@ -18,6 +19,7 @@ from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
UserModel = get_user_model()
|
||||
+logger = logging.getLogger("django.contrib.auth")
|
||||
|
||||
|
||||
def _unicode_ci_compare(s1, s2):
|
||||
@@ -256,7 +258,12 @@ class PasswordResetForm(forms.Form):
|
||||
html_email = loader.render_to_string(html_email_template_name, context)
|
||||
email_message.attach_alternative(html_email, 'text/html')
|
||||
|
||||
- email_message.send()
|
||||
+ try:
|
||||
+ email_message.send()
|
||||
+ except Exception:
|
||||
+ logger.exception(
|
||||
+ "Failed to send password reset email to %s:", context["user"].pk
|
||||
+ )
|
||||
|
||||
def get_users(self, email):
|
||||
"""Given an email, return matching user(s) who should receive a reset.
|
||||
Index: Django-2.2.28/docs/releases/4.2.16.txt
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/docs/releases/4.2.16.txt
|
||||
+++ Django-2.2.28/docs/releases/4.2.16.txt
|
||||
@@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-serv
|
||||
:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
|
||||
denial-of-service attack via very large inputs with a specific sequence of
|
||||
characters.
|
||||
+
|
||||
+CVE-2024-45231: Potential user email enumeration via response status on password reset
|
||||
+======================================================================================
|
||||
+
|
||||
+Due to unhandled email sending failures, the
|
||||
+:class:`~django.contrib.auth.forms.PasswordResetForm` class allowed remote
|
||||
+attackers to enumerate user emails by issuing password reset requests and
|
||||
+observing the outcomes.
|
||||
+
|
||||
+To mitigate this risk, exceptions occurring during password reset email sending
|
||||
+are now handled and logged using the :ref:`django-contrib-auth-logger` logger.
|
||||
Index: Django-2.2.28/docs/topics/auth/default.txt
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/docs/topics/auth/default.txt
|
||||
+++ Django-2.2.28/docs/topics/auth/default.txt
|
||||
@@ -1530,7 +1530,9 @@ provides several built-in forms located
|
||||
.. method:: send_mail(subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name=None)
|
||||
|
||||
Uses the arguments to send an ``EmailMultiAlternatives``.
|
||||
- Can be overridden to customize how the email is sent to the user.
|
||||
+ Can be overridden to customize how the email is sent to the user. If
|
||||
+ you choose to override this method, be mindful of handling potential
|
||||
+ exceptions raised due to email sending failures.
|
||||
|
||||
:param subject_template_name: the template for the subject.
|
||||
:param email_template_name: the template for the email body.
|
||||
Index: Django-2.2.28/tests/auth_tests/test_forms.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/auth_tests/test_forms.py
|
||||
+++ Django-2.2.28/tests/auth_tests/test_forms.py
|
||||
@@ -929,6 +929,27 @@ class PasswordResetFormTest(TestDataMixi
|
||||
message.get_payload(1).get_payload()
|
||||
))
|
||||
|
||||
+ @override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend")
|
||||
+ def test_save_send_email_exceptions_are_catched_and_logged(self):
|
||||
+ (user, username, email) = self.create_dummy_user()
|
||||
+ form = PasswordResetForm({"email": email})
|
||||
+ self.assertTrue(form.is_valid())
|
||||
+
|
||||
+ with self.assertLogs("django.contrib.auth", level=0) as cm:
|
||||
+ form.save()
|
||||
+
|
||||
+ self.assertEqual(len(mail.outbox), 0)
|
||||
+ self.assertEqual(len(cm.output), 1)
|
||||
+ errors = cm.output[0].split("\n")
|
||||
+ pk = user.pk
|
||||
+ self.assertEqual(
|
||||
+ errors[0],
|
||||
+ f"ERROR:django.contrib.auth:Failed to send password reset email to {pk}:",
|
||||
+ )
|
||||
+ self.assertEqual(
|
||||
+ errors[-1], "ValueError: FailingEmailBackend is doomed to fail."
|
||||
+ )
|
||||
+
|
||||
@override_settings(AUTH_USER_MODEL='auth_tests.CustomEmailField')
|
||||
def test_custom_email_field(self):
|
||||
email = 'test@mail.com'
|
||||
Index: Django-2.2.28/tests/mail/custombackend.py
|
||||
===================================================================
|
||||
--- Django-2.2.28.orig/tests/mail/custombackend.py
|
||||
+++ Django-2.2.28/tests/mail/custombackend.py
|
||||
@@ -13,3 +13,8 @@ class EmailBackend(BaseEmailBackend):
|
||||
# Messages are stored in an instance variable for testing.
|
||||
self.test_outbox.extend(email_messages)
|
||||
return len(email_messages)
|
||||
+
|
||||
+
|
||||
+class FailingEmailBackend(BaseEmailBackend):
|
||||
+ def send_messages(self, email_messages):
|
||||
+ raise ValueError("FailingEmailBackend is doomed to fail.")
|
||||
@ -1,7 +1,7 @@
|
||||
%global _empty_manifest_terminate_build 0
|
||||
Name: python-django
|
||||
Version: 2.2.27
|
||||
Release: 11
|
||||
Release: 12
|
||||
Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
|
||||
License: Apache-2.0 and Python-2.0 and OFL-1.1 and MIT
|
||||
URL: https://www.djangoproject.com/
|
||||
@ -25,6 +25,17 @@ Patch8: CVE-2023-46695.patch
|
||||
Patch9: CVE-2024-24680.patch
|
||||
# https://github.com/django/django/commit/072963e4c4d0b3a7a8c5412bc0c7d27d1a9c3521
|
||||
Patch10: CVE-2024-27351.patch
|
||||
# patch11-20 origin: https://build.opensuse.org/package/show/openSUSE:Backports:SLE-15-SP5:Update/python-Django
|
||||
Patch11: CVE-2024-38875.patch
|
||||
Patch12: CVE-2024-39329.patch
|
||||
Patch13: CVE-2024-39330.patch
|
||||
Patch14: CVE-2024-39614.patch
|
||||
Patch15: CVE-2024-41989.patch
|
||||
Patch16: CVE-2024-41990.patch
|
||||
Patch17: CVE-2024-41991.patch
|
||||
Patch18: CVE-2024-42005.patch
|
||||
Patch19: CVE-2024-45230.patch
|
||||
Patch20: CVE-2024-45231.patch
|
||||
|
||||
BuildArch: noarch
|
||||
%description
|
||||
@ -91,6 +102,10 @@ mv %{buildroot}/doclist.lst .
|
||||
%{_docdir}/*
|
||||
|
||||
%changelog
|
||||
* Fri Oct 11 2024 wangkai <13474090681@163.com> - 2.2.27-12
|
||||
- Fix CVE-2024-38875 CVE-2024-39329 CVE-2024-39330 CVE-2024-39614 CVE-2024-41989
|
||||
CVE-2024-41990 CVE-2024-41991 CVE-2024-42005 CVE-2024-45230 CVE-2024-45231
|
||||
|
||||
* Tue Mar 05 2024 yaoxin <yao_xin001@hoperun.com> - 2.2.27-11
|
||||
- Fix CVE-2024-27351
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user