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()

Reply via email to