Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-Flask-Cors for openSUSE:Factory checked in at 2026-04-02 17:41:17 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-Flask-Cors (Old) and /work/SRC/openSUSE:Factory/.python-Flask-Cors.new.21863 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-Flask-Cors" Thu Apr 2 17:41:17 2026 rev:14 rq:1344234 version:6.0.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-Flask-Cors/python-Flask-Cors.changes 2025-05-05 22:59:11.227510298 +0200 +++ /work/SRC/openSUSE:Factory/.python-Flask-Cors.new.21863/python-Flask-Cors.changes 2026-04-02 17:42:29.494640971 +0200 @@ -1,0 +2,16 @@ +Wed Apr 1 22:28:19 UTC 2026 - Dirk Müller <[email protected]> + +- update to 6.0.2 (bsc#1239846, CVE-2024-6839, + bsc#1239847, CVE-2024-6844, + bsc#1239848, CVE-2024-6866): + * Invert regex sorting to make it correctly match the intent + (sorting by specificity descending) #391 + * Path specificity ordering has changed to improve specificity. + This may break users who expected the previous incorrect + ordering. + * [CVE-2024-6839] Sort Paths by Regex Specificity + * [CVE-2024-6844] Replace use of (urllib) unquote_plus with + unquote + * [CVE-2024-6866] Case Sensitive Request Path Matching + +------------------------------------------------------------------- Old: ---- flask_cors-5.0.1.tar.gz New: ---- flask_cors-6.0.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-Flask-Cors.spec ++++++ --- /var/tmp/diff_new_pack.qgpINP/_old 2026-04-02 17:42:30.994702924 +0200 +++ /var/tmp/diff_new_pack.qgpINP/_new 2026-04-02 17:42:31.022704080 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-Flask-Cors # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,13 +18,14 @@ %{?sle15_python_module_pythons} Name: python-Flask-Cors -Version: 5.0.1 +Version: 6.0.2 Release: 0 Summary: A Flask extension adding a decorator for CORS support License: MIT URL: https://github.com/corydolphin/flask-cors Source: https://github.com/corydolphin/flask-cors/archive/refs/tags/%{version}.tar.gz#/flask_cors-%{version}.tar.gz BuildRequires: %{python_module Flask >= 0.9} +BuildRequires: %{python_module Werkzeug >= 0.7} BuildRequires: %{python_module base >= 3.9} BuildRequires: %{python_module pip} BuildRequires: %{python_module pytest} ++++++ flask_cors-5.0.1.tar.gz -> flask_cors-6.0.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flask-cors-5.0.1/flask_cors/core.py new/flask-cors-6.0.2/flask_cors/core.py --- old/flask-cors-5.0.1/flask_cors/core.py 2025-02-24 04:51:54.000000000 +0100 +++ new/flask-cors-6.0.2/flask_cors/core.py 2025-12-12 18:55:57.000000000 +0100 @@ -69,14 +69,17 @@ # 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)) + # 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=pattern_length, reverse=True) + return sorted(resources, key=sort_key) elif isinstance(resources, str): return [(re_fix(resources), {})] @@ -121,9 +124,10 @@ 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.", ) @@ -164,7 +168,7 @@ 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)) @@ -277,22 +281,31 @@ 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flask-cors-5.0.1/flask_cors/extension.py new/flask-cors-6.0.2/flask_cors/extension.py --- old/flask-cors-5.0.1/flask_cors/extension.py 2025-02-24 04:51:54.000000000 +0100 +++ new/flask-cors-6.0.2/flask_cors/extension.py 2025-12-12 18:55:57.000000000 +0100 @@ -1,9 +1,9 @@ import logging -from urllib.parse import unquote_plus +from urllib.parse import unquote from flask import request -from .core import ACL_ORIGIN, get_cors_options, get_regexp_pattern, parse_resources, set_cors_headers, try_match +from .core import ACL_ORIGIN, get_cors_options, get_regexp_pattern, parse_resources, set_cors_headers, try_match_pattern LOG = logging.getLogger(__name__) @@ -188,9 +188,9 @@ 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 '%r' matches CORS resource '%s'. Using options: %s", request.path, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flask-cors-5.0.1/pyproject.toml new/flask-cors-6.0.2/pyproject.toml --- old/flask-cors-5.0.1/pyproject.toml 2025-02-24 04:51:54.000000000 +0100 +++ new/flask-cors-6.0.2/pyproject.toml 2025-12-12 18:55:57.000000000 +0100 @@ -3,8 +3,9 @@ version = "0.0.1" description = "A Flask extension simplifying CORS support" authors = [{ name = "Cory Dolphin", email = "[email protected]" }] -readme = "README.md" +readme = "README.rst" keywords = ['python'] +license = "MIT" requires-python = ">=3.9,<4.0" classifiers = [ "Intended Audience :: Developers", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flask-cors-5.0.1/tests/core/helper_tests.py new/flask-cors-6.0.2/tests/core/helper_tests.py --- old/flask-cors-5.0.1/tests/core/helper_tests.py 2025-02-24 04:51:54.000000000 +0100 +++ new/flask-cors-6.0.2/tests/core/helper_tests.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -""" - Tests for helper and utility methods - TODO: move integration tests (e.g. all that test a full request cycle) - into smaller, broken-up unit tests to simplify testing. - ~~~~ - Flask-CORS is a simple extension to Flask allowing you to support cross - origin resource sharing (CORS) using a simple decorator. - - :copyright: (c) 2016 by Cory Dolphin. - :license: MIT, see LICENSE for more details. -""" - -import unittest - -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_flexible_str_str(self): - self.assertEqual(flexible_str('Bar, Foo, Qux'), 'Bar, Foo, Qux') - - def test_flexible_str_set(self): - self.assertEqual(flexible_str({'Foo', 'Bar', 'Qux'}), - 'Bar, Foo, Qux') - - def test_serialize_options(self): - try: - serialize_options({ - 'origins': r'*', - 'allow_headers': True, - 'supports_credentials': True, - 'send_wildcard': True - }) - self.assertFalse(True, "A Value Error should have been raised.") - except ValueError: - pass - - def test_get_allow_headers_empty(self): - options = serialize_options({'allow_headers': r'*'}) - - self.assertEqual(get_allow_headers(options, ''), None) - self.assertEqual(get_allow_headers(options, None), None) - - def test_get_allow_headers_matching(self): - options = serialize_options({'allow_headers': r'*'}) - - self.assertEqual(get_allow_headers(options, 'X-FOO'), 'X-FOO') - self.assertEqual( - get_allow_headers(options, 'X-Foo, X-Bar'), - 'X-Bar, X-Foo' - ) - - def test_get_allow_headers_matching_none(self): - options = serialize_options({'allow_headers': r'X-FLASK-.*'}) - - self.assertEqual(get_allow_headers(options, 'X-FLASK-CORS'), - 'X-FLASK-CORS') - self.assertEqual( - get_allow_headers(options, 'X-NOT-FLASK-CORS'), - '' - ) - - def test_parse_resources_sorted(self): - resources = parse_resources({ - '/foo': {'origins': 'http://foo.com'}, - re.compile(r'/.*'): { - 'origins': 'http://some-domain.com' - }, - re.compile(r'/api/v1/.*'): { - 'origins': 'http://specific-domain.com' - } - }) - - self.assertEqual( - [r[0] for r in resources], - [re.compile(r'/api/v1/.*'), '/foo', re.compile(r'/.*')] - ) - - def test_probably_regex(self): - self.assertTrue(probably_regex("http://*.example.com")) - self.assertTrue(probably_regex("*")) - self.assertFalse(probably_regex("http://example.com")) - self.assertTrue(probably_regex(r"http://[\w].example.com")) - self.assertTrue(probably_regex(r"http://\w+.example.com")) - self.assertTrue(probably_regex("https?://example.com")) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flask-cors-5.0.1/tests/core/test_helpers.py new/flask-cors-6.0.2/tests/core/test_helpers.py --- old/flask-cors-5.0.1/tests/core/test_helpers.py 1970-01-01 01:00:00.000000000 +0100 +++ new/flask-cors-6.0.2/tests/core/test_helpers.py 2025-12-12 18:55:57.000000000 +0100 @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +""" + Tests for helper and utility methods + TODO: move integration tests (e.g. all that test a full request cycle) + into smaller, broken-up unit tests to simplify testing. + ~~~~ + Flask-CORS is a simple extension to Flask allowing you to support cross + origin resource sharing (CORS) using a simple decorator. + + :copyright: (c) 2016 by Cory Dolphin. + :license: MIT, see LICENSE for more details. +""" + +import unittest + +from flask_cors.core import * + + +class InternalsTestCase(unittest.TestCase): + 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.assertEqual(flexible_str('Bar, Foo, Qux'), 'Bar, Foo, Qux') + + def test_flexible_str_set(self): + self.assertEqual(flexible_str({'Foo', 'Bar', 'Qux'}), + 'Bar, Foo, Qux') + + def test_serialize_options(self): + try: + serialize_options({ + 'origins': r'*', + 'allow_headers': True, + 'supports_credentials': True, + 'send_wildcard': True + }) + self.assertFalse(True, "A Value Error should have been raised.") + except ValueError: + pass + + def test_get_allow_headers_empty(self): + options = serialize_options({'allow_headers': r'*'}) + + self.assertEqual(get_allow_headers(options, ''), None) + self.assertEqual(get_allow_headers(options, None), None) + + def test_get_allow_headers_matching(self): + options = serialize_options({'allow_headers': r'*'}) + + self.assertEqual(get_allow_headers(options, 'X-FOO'), 'X-FOO') + self.assertEqual( + get_allow_headers(options, 'X-Foo, X-Bar'), + 'X-Bar, X-Foo' + ) + + def test_get_allow_headers_matching_none(self): + options = serialize_options({'allow_headers': r'X-FLASK-.*'}) + + self.assertEqual(get_allow_headers(options, 'X-FLASK-CORS'), + 'X-FLASK-CORS') + self.assertEqual( + get_allow_headers(options, 'X-NOT-FLASK-CORS'), + '' + ) + + def test_parse_resources_sorted(self): + resources = parse_resources({ + '/foo': {'origins': 'http://foo.com'}, + re.compile(r'/.*'): { + 'origins': 'http://some-domain.com' + }, + re.compile(r'/api/v1/.*'): { + 'origins': 'http://specific-domain.com' + } + }) + + self.assertEqual( + [r[0] for r in resources], + ['/foo', re.compile(r'/api/v1/.*'), re.compile(r'/.*')] + ) + + def test_probably_regex(self): + self.assertTrue(probably_regex("http://*.example.com")) + self.assertTrue(probably_regex("*")) + self.assertFalse(probably_regex("http://example.com")) + self.assertTrue(probably_regex(r"http://[\w].example.com")) + self.assertTrue(probably_regex(r"http://\w+.example.com")) + self.assertTrue(probably_regex("https?://example.com")) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/flask-cors-5.0.1/tests/extension/test_app_extension.py new/flask-cors-6.0.2/tests/extension/test_app_extension.py --- old/flask-cors-5.0.1/tests/extension/test_app_extension.py 2025-02-24 04:51:54.000000000 +0100 +++ new/flask-cors-6.0.2/tests/extension/test_app_extension.py 2025-12-12 18:55:57.000000000 +0100 @@ -378,5 +378,61 @@ 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()
