Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions flask_cors/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def get_cors_origins(options, request_origin):
# 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):
elif try_match_any_pattern(request_origin, origins, caseSensitive=False, enforceEndOfString=True):
LOG.debug(
"The request's Origin header matches. Sending CORS headers.",
)
Expand Down Expand Up @@ -281,22 +281,25 @@ def re_fix(reg):
return r".*" if reg == r"*" else reg


def try_match_any_pattern(inst, patterns, caseSensitive=True):
return any(try_match_pattern(inst, pattern, caseSensitive) for pattern in patterns)
def try_match_any_pattern(inst, patterns, caseSensitive=True, enforceEndOfString=False):
return any(try_match_pattern(inst, pattern, caseSensitive, enforceEndOfString) for pattern in patterns)

def try_match_pattern(value, pattern, caseSensitive=True):
def try_match_pattern(value, pattern, caseSensitive=True, enforceEndOfString=False):
"""
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
The value of enforceEndOfString should be true when using for origins
"""
if isinstance(pattern, RegexObject):
return re.match(pattern, value)
if probably_regex(pattern):
flags = 0 if caseSensitive else re.IGNORECASE
try:
if enforceEndOfString and not pattern.endswith(("$", "\\Z")):
pattern = pattern + "\\Z"
return re.match(pattern, value, flags=flags)
except re.error:
return False
Expand Down
5 changes: 5 additions & 0 deletions flask_cors/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ def cross_origin(*args, **kwargs):
The origin(s) may be regular expressions, case-sensitive strings,
or else an asterisk

.. note:

When using regexes, ensure that you assert the position at the
end of the string

Default : '*'
:type origins: list, string or regex

Expand Down
7 changes: 6 additions & 1 deletion flask_cors/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,15 @@ class CORS:

.. note::

origins must include the schema and the port (if not port 80),
origins must include the scheme and the port (if different than default),
e.g.,
`CORS(app, origins=["http://localhost:8000", "https://example.com"])`.

when using regexes, escape needed characters and assert end of string
e.g.,
`CORS(app, origins=[r"https://.*\\.example\\.com\\Z"])
And not `r"https://.*\\.example\\.com"` which would allow `https://a.example.com.attacker.com`

Default : '*'
:type origins: list, string or regex

Expand Down
22 changes: 17 additions & 5 deletions tests/decorator/test_origins.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,22 @@ def test_set():
return 'Welcome!'

@self.app.route('/test_subdomain_regex')
@cross_origin(origins=r"http?://\w*\.?example\.com:?\d*/?.*")
@cross_origin(origins=r"https?://\w*\.?example\.com:?\d*\Z")
def test_subdomain_regex():
return ''

@self.app.route('/test_compiled_subdomain_regex')
@cross_origin(origins=re.compile(r"http?://\w*\.?example\.com:?\d*/?.*"))
@cross_origin(origins=re.compile(r"https?://\w*\.?example\.com:?\d*\Z"))
def test_compiled_subdomain_regex():
return ''

@self.app.route('/test_regex_list')
@cross_origin(origins=[r".*.example.com", r".*.otherexample.com"])
@cross_origin(origins=[r"https?://.*\.example\.com\Z", r"https?://.*\.otherexample.com\Z"])
def test_regex_list():
return ''

@self.app.route('/test_regex_mixed_list')
@cross_origin(origins=["http://example.com", r".*.otherexample.com"])
@cross_origin(origins=["http://example.com", r"https?://.*\.otherexample\.com\Z"])
def test_regex_mixed_list():
return ''

Expand Down Expand Up @@ -146,6 +146,8 @@ def test_set_serialized(self):
def test_not_matching_origins(self):
for resp in self.iter_responses('/test_list',origin="http://bazz.com"):
self.assertFalse(ACL_ORIGIN in resp.headers)
for resp in self.iter_responses('/test_list',origin="http://bar.com.attacker.com"):
self.assertFalse(ACL_ORIGIN in resp.headers)

def test_subdomain_regex(self):
for sub in letters:
Expand All @@ -164,10 +166,18 @@ def test_compiled_subdomain_regex(self):
def test_regex_list(self):
for parent in 'example.com', 'otherexample.com':
for sub in letters:
domain = "http://{}.{}.com".format(sub, parent)
domain = "http://{}.{}".format(sub, parent)
for resp in self.iter_responses('/test_regex_list',
headers={'origin': domain}):
self.assertEqual(domain, resp.headers.get(ACL_ORIGIN))

def test_regex_list_unallowed_origin(self):
for parent in 'example.com', 'otherexample.com':
for sub in letters:
domain = "http://{}.{}.com.attacker.com".format(sub, parent)
for resp in self.iter_responses('/test_regex_list',
headers={'origin': domain}):
self.assertFalse(ACL_ORIGIN in resp.headers)

def test_regex_mixed_list(self):
'''
Expand Down Expand Up @@ -198,6 +208,8 @@ def test_multiple_protocols(self):
logging.getLogger('flask_cors').level = logging.DEBUG
resp = self.get('test_multiple_protocols', origin='https://example.com')
self.assertEqual('https://example.com', resp.headers.get(ACL_ORIGIN))
resp = self.options('test_multiple_protocols', origin='https://example.com.attacker.com')
self.assertFalse(ACL_ORIGIN in resp.headers)


if __name__ == "__main__":
Expand Down
10 changes: 5 additions & 5 deletions tests/extension/test_app_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ def setUp(self):
'origins': {"http://foo.com", "http://bar.com"}
},
r'/test_subdomain_regex': {
'origins': r"http?://\w*\.?example\.com:?\d*/?.*"
'origins': r"https?://\w*\.?example\.com:?\d*\Z"
},
r'/test_regex_list': {
'origins': [r".*.example.com", r".*.otherexample.com"]
'origins': [r"http://.*\.example\.com\Z", r"http://.*\.otherexample.com\Z"]
},
r'/test_regex_mixed_list': {
'origins': ["http://example.com", r".*.otherexample.com"]
'origins': ["http://example.com", r"http://.*\.otherexample\.com\Z"]
},
r'/test_send_wildcard_with_origin' : {
'send_wildcard':True
},
re.compile(r'/test_compiled_subdomain_\w*'): {
'origins': re.compile(r"http://example\d+.com")
'origins': re.compile(r"http://example\d+.com\Z")
},
r'/test_defaults':{}
})
Expand Down Expand Up @@ -136,7 +136,7 @@ def test_compiled_subdomain_regex(self):
def test_regex_list(self):
for parent in 'example.com', 'otherexample.com':
for sub in letters:
domain = "http://{}.{}.com".format(sub, parent)
domain = "http://{}.{}".format(sub, parent)
for resp in self.iter_responses('/test_regex_list',
headers={'origin': domain}):
self.assertEqual(domain, resp.headers.get(ACL_ORIGIN))
Expand Down