1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
|
From: Adriano Sela Aviles <adriano.selaviles@gmail.com>
Date: Wed, 14 May 2025 21:13:34 -0700
Subject: [PATCH] [CVE-2024-6866] Case Sensitive Request Path Matching (#390)
* [CVE-2024-6866] Case Sensitive Request Path Matching
* Update flask_cors/core.py
Co-authored-by: Cory Dolphin <corydolphin@gmail.com>
Reviewed-By: Daniel Leidert <dleidert@debian.org>
Origin: https://github.com/corydolphin/flask-cors/commit/eb39516a3c96b90d0ae5f51293972395ec3ef358
Bug: https://github.com/corydolphin/flask-cors/pull/390
Bug-Debian: https://bugs.debian.org/1100988
Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2024-6866
Bug-Freexian-Security: https://deb.freexian.com/extended-lts/tracker/CVE-2024-6866
---
flask_cors/core.py | 47 +++++++++++++++++++++++++++-------------------
flask_cors/extension.py | 2 +-
tests/core/helper_tests.py | 9 ++++++---
3 files changed, 35 insertions(+), 23 deletions(-)
diff --git a/flask_cors/core.py b/flask_cors/core.py
index 7654cb1..ac9682d 100644
--- a/flask_cors/core.py
+++ b/flask_cors/core.py
@@ -123,9 +123,10 @@ def get_cors_origins(options, request_origin):
if wildcard and options.get('send_wildcard'):
LOG.debug("Allowed origins are set to '*'. Sending wildcard CORS header.")
return ['*']
- # If the value of the Origin header is a case-sensitive match
- # for any of the values in list of origins
- elif try_match_any(request_origin, origins):
+ # If the value of the Origin header is a case-insensitive match
+ # for any of the values in list of origins.
+ # NOTE: Per RFC 1035 and RFC 4343 schemes and hostnames are case insensitive.
+ elif try_match_any_pattern(request_origin, origins, caseSensitive=False):
LOG.debug("The request's Origin header matches. Sending CORS headers.", )
# Add a single Access-Control-Allow-Origin header, with either
# the value of the Origin header or the string "*" as value.
@@ -163,10 +164,7 @@ def get_allow_headers(options, acl_request_headers):
request_headers = [h.strip() for h in acl_request_headers.split(',')]
# any header that matches in the allow_headers
- matching_headers = filter(
- lambda h: try_match_any(h, options.get('allow_headers')),
- request_headers
- )
+ matching_headers = filter(lambda h: try_match_any_pattern(h, options.get("allow_headers"), caseSensitive=False), request_headers)
return ', '.join(sorted(matching_headers))
@@ -268,21 +266,32 @@ def re_fix(reg):
return r'.*' if reg == r'*' else reg
-def try_match_any(inst, patterns):
- return any(try_match(inst, pattern) for pattern in patterns)
+def try_match_any_pattern(inst, patterns, caseSensitive=True):
+ return any(try_match_pattern(inst, pattern, caseSensitive) for pattern in patterns)
-def try_match(request_origin, maybe_regex):
- """Safely attempts to match a pattern or string to a request origin."""
- if isinstance(maybe_regex, RegexObject):
- return re.match(maybe_regex, request_origin)
- elif probably_regex(maybe_regex):
- return re.match(maybe_regex, request_origin, flags=re.IGNORECASE)
- else:
+def try_match_pattern(value, pattern, caseSensitive=True):
+ """
+ Safely attempts to match a pattern or string to a value. This
+ function can be used to match request origins, headers, or paths.
+ The value of caseSensitive should be set in accordance to the
+ data being compared e.g. origins and headers are case insensitive
+ whereas paths are case-sensitive
+ """
+ if isinstance(pattern, RegexObject):
+ return re.match(pattern, value)
+ if probably_regex(pattern):
+ flags = 0 if caseSensitive else re.IGNORECASE
try:
- return request_origin.lower() == maybe_regex.lower()
- except AttributeError:
- return request_origin == maybe_regex
+ return re.match(pattern, value, flags=flags)
+ except re.error:
+ return False
+ try:
+ v = str(value)
+ p = str(pattern)
+ return v == p if caseSensitive else v.casefold() == p.casefold()
+ except Exception:
+ return value == pattern
def get_cors_options(appInstance, *dicts):
diff --git a/flask_cors/extension.py b/flask_cors/extension.py
index 77cf47d..44b0a09 100644
--- a/flask_cors/extension.py
+++ b/flask_cors/extension.py
@@ -179,7 +179,7 @@ def make_after_request_function(resources):
return resp
normalized_path = unquote_plus(request.path)
for res_regex, res_options in resources:
- if try_match(normalized_path, res_regex):
+ if try_match_pattern(normalized_path, res_regex, caseSensitive=True):
LOG.debug("Request to '%r' matches CORS resource '%s'. Using options: %s",
request.path, get_regexp_pattern(res_regex), res_options)
set_cors_headers(resp, res_options)
diff --git a/tests/core/helper_tests.py b/tests/core/helper_tests.py
index d7811da..4eedca8 100644
--- a/tests/core/helper_tests.py
+++ b/tests/core/helper_tests.py
@@ -17,9 +17,12 @@ from flask_cors.core import *
class InternalsTestCase(unittest.TestCase):
- def test_try_match(self):
- self.assertFalse(try_match('www.com/foo', 'www.com/fo'))
- self.assertTrue(try_match('www.com/foo', 'www.com/fo*'))
+ def test_try_match_pattern(self):
+ self.assertFalse(try_match_pattern('www.com/foo', 'www.com/fo', caseSensitive=True))
+ self.assertTrue(try_match_pattern('www.com/foo', 'www.com/fo*', caseSensitive=True))
+ self.assertTrue(try_match_pattern('www.com', 'WwW.CoM', caseSensitive=False))
+ self.assertTrue(try_match_pattern('/foo', '/fo*', caseSensitive=True))
+ self.assertFalse(try_match_pattern('/foo', '/Fo*', caseSensitive=True))
def test_flexible_str_str(self):
self.assertEquals(flexible_str('Bar, Foo, Qux'), 'Bar, Foo, Qux')
|