Package: python3-flask-cors Followup-For: Bug #1100988 Control: tags -1 patch
Dear Maintainer, This is a non-maintainer patch submission to fix three CVEs in the Bookworm version of python-flask-cors (3.0.10-2): - CVE-2024-6839: Improper CORS validation under certain path conditions - CVE-2024-6844: Case-insensitive path matching issue - CVE-2024-6866: CORS bypass via trailing path segment The attached patch is backported from upstream and applies to the current version in Bookworm. It is submitted as a single quilt patch following DEP-3 format, and the changelog entry uses proper NMU versioning: Version: 3.0.10-2.1~deb12u1 Distribution: bookworm-security The patch builds cleanly in a Bookworm environment and passes lintian with only informational tags. Please consider applying this fix via `bookworm-security`. Thank you. -- Yang Wang -- System Information: Debian Release: 12.11 APT prefers stable APT policy: (500, 'stable') merged-usr: no Architecture: amd64 (x86_64) Kernel: Linux 6.8.0-60-generic (SMP w/8 CPU threads; PREEMPT) Locale: LANG=C, LC_CTYPE=C (charmap=ANSI_X3.4-1968) (ignored: LC_ALL set to C), LANGUAGE not set Shell: /bin/sh linked to /bin/dash Init: unable to detect Versions of packages python3-flask-cors depends on: ii libjs-sphinxdoc 5.3.0-4 ii python3 3.11.2-1+b1 ii python3-flask 2.2.2-3 ii python3-six 1.16.0-4 ii sphinx-rtd-theme-common 1.2.0+dfsg-1 python3-flask-cors recommends no packages. python3-flask-cors suggests no packages. -- no debconf information
diff -Nru python-flask-cors-3.0.10/debian/changelog python-flask-cors-3.0.10/debian/changelog --- python-flask-cors-3.0.10/debian/changelog 2023-01-22 08:52:05.000000000 +0000 +++ python-flask-cors-3.0.10/debian/changelog 2025-06-17 17:59:48.000000000 +0000 @@ -1,3 +1,15 @@ +python-flask-cors (3.0.10-2.1~deb12u1) bookworm-security; urgency=medium + + * Non-maintainer upload. + * Fix CVE-2024-6839, CVE-2024-6844, CVE-2024-6866: + - Improper CORS validation + - Path matching logic bugs + - CORS bypass with trailing slashes + * Backported from upstream fixes. + (Closes: #1100988) + + -- Yang Wang <yang.w...@windriver.com> Tue, 17 Jun 2025 17:59:48 +0000 + python-flask-cors (3.0.10-2) unstable; urgency=medium * Team upload. diff -Nru python-flask-cors-3.0.10/debian/patches/series python-flask-cors-3.0.10/debian/patches/series --- python-flask-cors-3.0.10/debian/patches/series 2022-11-01 07:15:06.000000000 +0000 +++ python-flask-cors-3.0.10/debian/patches/series 2025-06-17 15:43:51.000000000 +0000 @@ -3,3 +3,4 @@ upstream/Spelling-Fix-misspelled-word-conjuction.patch upstream/Spelling-Fix-misspelled-word-maching.patch debian-hacks/docs-Use-local-inventory-for-Python3.patch +upstream/fix-flask-cors-CVE-2024-6839_6844_6866.patch diff -Nru python-flask-cors-3.0.10/debian/patches/upstream/fix-flask-cors-CVE-2024-6839_6844_6866.patch python-flask-cors-3.0.10/debian/patches/upstream/fix-flask-cors-CVE-2024-6839_6844_6866.patch --- python-flask-cors-3.0.10/debian/patches/upstream/fix-flask-cors-CVE-2024-6839_6844_6866.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-flask-cors-3.0.10/debian/patches/upstream/fix-flask-cors-CVE-2024-6839_6844_6866.patch 2025-06-17 17:59:39.000000000 +0000 @@ -0,0 +1,241 @@ +Description: Backport fixes for CVE-2024-6839, CVE-2024-6844, CVE-2024-6866 + These vulnerabilities affected older versions of Flask-CORS prior to 6.0.0. + This patch ports upstream changes to 3.0.10 used in Debian Bookworm. +Author: Backport by Yang Wang +Origin: upstream, https://github.com/corydolphin/flask-cors/compare/5.0.1...6.0.0 +Bug-Debian: https://bugs.debian.org/1100988 +Forwarded: not-needed +Last-Update: 2025-06-17 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +Index: python-flask-cors-3.0.10/flask_cors/core.py +=================================================================== +--- python-flask-cors-3.0.10.orig/flask_cors/core.py ++++ python-flask-cors-3.0.10/flask_cors/core.py +@@ -69,16 +69,17 @@ def parse_resources(resources): + # resource of '*', which is not actually a valid regexp. + resources = [(re_fix(k), v) for k, v in resources.items()] + +- # Sort by regex length to provide consistency of matching and +- # to provide a proxy for specificity of match. E.G. longer +- # regular expressions are tried first. +- def pattern_length(pair): +- maybe_regex, _ = pair +- return len(get_regexp_pattern(maybe_regex)) +- +- return sorted(resources, +- key=pattern_length, +- reverse=True) ++ # Sort patterns with static (literal) paths first, then by regex specificity ++ def sort_key(pair): ++ pattern, _ = pair ++ if isinstance(pattern, RegexObject): ++ return (1, 0, pattern.pattern.count("/"), -len(pattern.pattern)) ++ elif probably_regex(pattern): ++ return (1, 1, pattern.count("/"), -len(pattern)) ++ else: ++ return (0, 0, pattern.count("/"), -len(pattern)) ++ ++ return sorted(resources, key=sort_key) + + elif isinstance(resources, string_types): + return [(re_fix(resources), {})] +@@ -123,10 +124,13 @@ def get_cors_origins(options, request_or + 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): +- LOG.debug("The request's Origin header matches. Sending CORS headers.", ) ++ # If the value of the Origin header is a case-insensitive match ++ # for any of the values in list of origins.Add commentMore actions ++ # 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. + # -- W3Spec +@@ -163,10 +167,7 @@ def get_allow_headers(options, acl_reque + 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,22 +269,31 @@ 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): + """ +Index: python-flask-cors-3.0.10/flask_cors/extension.py +=================================================================== +--- python-flask-cors-3.0.10.orig/flask_cors/extension.py ++++ python-flask-cors-3.0.10/flask_cors/extension.py +@@ -11,9 +11,9 @@ + from flask import request + from .core import * + try: +- from urllib.parse import unquote_plus ++ from urllib.parse import unquote + except ImportError: +- from urllib import unquote_plus ++ from urllib import unquote + + LOG = logging.getLogger(__name__) + +@@ -177,9 +177,9 @@ def make_after_request_function(resource + if resp.headers is not None and resp.headers.get(ACL_ORIGIN): + LOG.debug('CORS have been already evaluated, skipping') + return resp +- normalized_path = unquote_plus(request.path) ++ normalized_path = unquote(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 '%s' matches CORS resource '%s'. Using options: %s", + request.path, get_regexp_pattern(res_regex), res_options) + set_cors_headers(resp, res_options) +Index: python-flask-cors-3.0.10/tests/core/helper_tests.py +=================================================================== +--- python-flask-cors-3.0.10.orig/tests/core/helper_tests.py ++++ python-flask-cors-3.0.10/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') +@@ -78,7 +81,7 @@ class InternalsTestCase(unittest.TestCas + + self.assertEqual( + [r[0] for r in resources], +- [re.compile(r'/api/v1/.*'), '/foo', re.compile(r'/.*')] ++ ['/foo', re.compile(r'/api/v1/.*'), re.compile(r'/.*')] + ) + + def test_probably_regex(self): +Index: python-flask-cors-3.0.10/tests/extension/test_app_extension.py +=================================================================== +--- python-flask-cors-3.0.10.orig/tests/extension/test_app_extension.py ++++ python-flask-cors-3.0.10/tests/extension/test_app_extension.py +@@ -378,5 +378,61 @@ class AppExtensionBadRegexp(FlaskCorsTes + self.assertEqual(resp.status_code, 200) + + ++class AppExtensionPlusInPath(FlaskCorsTestCase): ++ ''' ++ Regression test for CVE-2024-6844: ++ Ensures that we correctly differentiate '+' from ' ' in URL paths. ++ ''' ++ ++ def setUp(self): ++ self.app = Flask(__name__) ++ CORS(self.app, resources={ ++ r'/service\+path': {'origins': ['http://foo.com']}, ++ r'/service path': {'origins': ['http://bar.com']}, ++ }) ++ ++ @self.app.route('/service+path') ++ def plus_path(): ++ return 'plus' ++ ++ @self.app.route('/service path') ++ def space_path(): ++ return 'space' ++ ++ self.client = self.app.test_client() ++ ++ def test_plus_path_origin_allowed(self): ++ ''' ++ Ensure that CORS matches + literally and allows the correct origin ++ ''' ++ response = self.client.get('/service+path', headers={'Origin': 'http://foo.com'}) ++ self.assertEqual(response.status_code, 200) ++ self.assertEqual(response.headers.get(ACL_ORIGIN), 'http://foo.com') ++ ++ def test_space_path_origin_allowed(self): ++ ''' ++ Ensure that CORS treats /service path differently and allows correct origin ++ ''' ++ response = self.client.get('/service%20path', headers={'Origin': 'http://bar.com'}) ++ self.assertEqual(response.status_code, 200) ++ self.assertEqual(response.headers.get(ACL_ORIGIN), 'http://bar.com') ++ ++ def test_plus_path_rejects_other_origin(self): ++ ''' ++ Origin not allowed for + path should be rejected ++ ''' ++ response = self.client.get('/service+path', headers={'Origin': 'http://bar.com'}) ++ self.assertEqual(response.status_code, 200) ++ self.assertIsNone(response.headers.get(ACL_ORIGIN)) ++ ++ def test_space_path_rejects_other_origin(self): ++ ''' ++ Origin not allowed for space path should be rejected ++ ''' ++ response = self.client.get('/service%20path', headers={'Origin': 'http://foo.com'}) ++ self.assertEqual(response.status_code, 200) ++ self.assertIsNone(response.headers.get(ACL_ORIGIN)) ++ ++ + if __name__ == "__main__": + unittest.main()