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
28 changes: 25 additions & 3 deletions flask_cors/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
# to a view.
FLASK_CORS_EVALUATED = '_FLASK_CORS_EVALUATED'

# Invalid CORS response data
INVALID_CORS_STATUS_MIN = 400
INVALID_CORS_STATUS_MAX = 499
INVALID_CORS_DEFAULT_STATUS= 200
INVALID_CORS_RESPONSE_DATA = ['']

# Strange, but this gets the type of a compiled regex, which is otherwise not
# exposed in a public API.
RegexObject = type(re.compile(''))
Expand All @@ -62,7 +68,8 @@
vary_header=True,
resources=r'/*',
intercept_exceptions=True,
always_send=True)
always_send=True,
invalid_cors_status_code=INVALID_CORS_DEFAULT_STATUS)


def parse_resources(resources):
Expand Down Expand Up @@ -236,6 +243,12 @@ def set_cors_headers(resp, options):
callback
"""

invalid_cors_status_code = options.get('invalid_cors_status_code')
change_body = invalid_cors_status_code != INVALID_CORS_DEFAULT_STATUS
if (invalid_cors_status_code < INVALID_CORS_STATUS_MIN
or INVALID_CORS_STATUS_MAX < invalid_cors_status_code):
invalid_cors_status_code = INVALID_CORS_DEFAULT_STATUS

# If CORS has already been evaluated via the decorator, skip
if hasattr(resp, FLASK_CORS_EVALUATED):
LOG.debug('CORS have been already evaluated, skipping')
Expand All @@ -252,8 +265,17 @@ def set_cors_headers(resp, options):

LOG.debug('Settings CORS headers: %s', str(headers_to_set))

for k, v in headers_to_set.items():
resp.headers.add(k, v)
# Set response code to invalid_cors_status_code when CORS request is invalid.
# When invalid_cors_status_code is set to Client Error Response Code,
# changes response with status `invalid_cors_status_code` and body
# `INVALID_CORS_RESPONSE_DATA`.
# Or else changes nothing (response status is 200 and with body).
if (ACL_ORIGIN not in headers_to_set.keys() and change_body):
resp.status_code = invalid_cors_status_code
resp.response = INVALID_CORS_RESPONSE_DATA
else:
for k, v in headers_to_set.items():
resp.headers.add(k, v)

return resp

Expand Down
18 changes: 18 additions & 0 deletions flask_cors/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ def cross_origin(*args, **kwargs):
Default : True
:type automatic_options: bool

:param invalid_cors_status_code:
Response status code when CORS request is invalid.

According to the CORS spec documentation from
`WHATWG <setuptools pep8 six coverage docutils pygments packaging>`_,
any response status code can be returned for CORS request.
Sometimes, for security, you wish flask_cors just returns HTTP
reponse without processing the HTTP request.

If `invalid_cors_status_code` set to client error response, from 400
to 499, flask_cors just returns HTTP response with that status code
and process nothing to the HTTP request.
Or else, flask_cors returns HTTP response with status code 200 and
response content to the HTTP request.

Default : 200
:type vary_header: int

"""
_options = kwargs

Expand Down
19 changes: 19 additions & 0 deletions flask_cors/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ class CORS(object):

Default : True
:type vary_header: bool

:param invalid_cors_status_code:
Response status code when CORS request is invalid.

According to the CORS spec documentation from
`WHATWG <setuptools pep8 six coverage docutils pygments packaging>`_,
any response status code can be returned for CORS request.
Sometimes, for security, you wish flask_cors just returns HTTP
reponse without processing the HTTP request.

If `invalid_cors_status_code` set to client error response, from 400
to 499, flask_cors just returns HTTP response with that status code
and process nothing to the HTTP request.
Or else, flask_cors returns HTTP response with status code 200 and
response content to the HTTP request.

Default : 200
:type vary_header: int

"""

def __init__(self, app=None, **kwargs):
Expand Down
113 changes: 113 additions & 0 deletions tests/decorator/test_invalid_cors_response_status_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
"""
test
~~~~
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 ..base_test import FlaskCorsTestCase
from flask import Flask
from flask_cors import *
from flask_cors.core import *


class DecoratorInvalidCorsResponseStatusCode(FlaskCorsTestCase):
def setUp(self):
self.app = Flask(__name__)

@self.app.route('/with_no_options')
@cross_origin(origins=['http://valid.com'])
def with_no_options():
return 'NO OPTIONS!'

@self.app.route('/with_client_error_status')
@cross_origin(
origins=['http://valid.com'],
invalid_cors_status_code=403
)
def with_client_error_status():
return 'WITH 403!'

@self.app.route('/with_non_client_error_response')
@cross_origin(
origins=['http://valid.com'],
invalid_cors_status_code=304
)
def with_non_client_error_response():
return 'WITH 304!'

def test_with_no_options(self):
''' If no `invalid_cors_status_code` options set,
response code OK(200) will be returned
when invalid CORS request.
'''
for index, resp in enumerate(self.iter_responses(
'/with_no_options',
verbs=['get', 'head', 'options'],
origin='http://valid.com'
)):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), 'http://valid.com')
expected = u'NO OPTIONS!' if index == 0 else u''
self.assertEqual(resp.data.decode('utf-8'), expected)

for index, resp in enumerate(self.iter_responses(
'/with_no_options',
verbs=['get', 'head', 'options'],
origin='http://invalid.com'
)):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), None)
expected = u'NO OPTIONS!' if index == 0 else u''
self.assertEqual(resp.data.decode('utf-8'), expected)

def test_with_client_error_status(self):
''' If `invalid_cors_status_code` options set as 403,
which is Clinet Error Status Code, response code
FORBIDDEN(403) will be returned when invalid CORS
request.
'''
for index, resp in enumerate(self.iter_responses(
'/with_client_error_status',
verbs=['get', 'head', 'options'],
origin='http://valid.com'
)):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), 'http://valid.com')
expected = u'WITH 403!' if index == 0 else u''
self.assertEqual(resp.data.decode('utf-8'), expected)

for resp in self.iter_responses('/with_client_error_status', origin='http://invalid.com'):
self.assertEqual(resp.status_code, 403)
self.assertEqual(resp.headers.get(ACL_ORIGIN), None)
self.assertEqual(resp.data.decode('utf-8'), u'')

def test_with_non_client_error_status(self):
''' If `invalid_cors_status_code` options set as 200,
which is not Clinet Error Status Code, response
code OK(200) will be returned when invalid CORS
request.
'''
for index, resp in enumerate(self.iter_responses(
'/with_non_client_error_response',
verbs=['get', 'head', 'options'],
origin='http://valid.com'
)):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), 'http://valid.com')
expected = u'WITH 304!' if index == 0 else u''
self.assertEqual(resp.data.decode('utf-8'), expected)

for resp in self.iter_responses('/with_non_client_error_response', origin='http://invalid.com'):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), None)
self.assertEqual(resp.data.decode('utf-8'), u'')


if __name__ == "__main__":
unittest.main()
97 changes: 97 additions & 0 deletions tests/extension/test_app_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,5 +378,102 @@ def index():
self.assertEqual(resp.status_code, 200)


class AppExtensionInvalidCorsResponseStatusCode(FlaskCorsTestCase):
def setUp(self):
self.app = Flask(__name__)
CORS(self.app, resources={
r'/with_no_options': {
'origins': {'http://valid.com'}
},
r'/with_client_error_status': {
'origins': {'http://valid.com'},
'invalid_cors_status_code': 403
},
r'/with_non_client_error_response': {
'origins': {'http://valid.com'},
'invalid_cors_status_code': 304
},
})

@self.app.route('/with_no_options')
def with_no_options():
return 'NO OPTIONS!'

@self.app.route('/with_client_error_status')
def with_client_error_status():
return 'WITH 403!'

@self.app.route('/with_non_client_error_response')
def with_non_client_error_response():
return 'WITH 304!'

def test_with_no_options(self):
''' If no `invalid_cors_status_code` options set,
response code OK(200) will be returned
when invalid CORS request.
'''
for index, resp in enumerate(self.iter_responses(
'/with_no_options',
verbs=['get', 'head', 'options'],
origin='http://valid.com'
)):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), 'http://valid.com')
expected = u'NO OPTIONS!' if index == 0 else u''
self.assertEqual(resp.data.decode('utf-8'), expected)

for index, resp in enumerate(self.iter_responses(
'/with_no_options',
verbs=['get', 'head', 'options'],
origin='http://invalid.com'
)):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), None)
expected = u'NO OPTIONS!' if index == 0 else u''
self.assertEqual(resp.data.decode('utf-8'), expected)

def test_with_client_error_status(self):
''' If `invalid_cors_status_code` options set as 403,
which is Clinet Error Status Code, response code
FORBIDDEN(403) will be returned when invalid CORS
request.
'''
for index, resp in enumerate(self.iter_responses(
'/with_client_error_status',
verbs=['get', 'head', 'options'],
origin='http://valid.com'
)):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), 'http://valid.com')
expected = u'WITH 403!' if index == 0 else u''
self.assertEqual(resp.data.decode('utf-8'), expected)

for resp in self.iter_responses('/with_client_error_status', origin='http://invalid.com'):
self.assertEqual(resp.status_code, 403)
self.assertEqual(resp.headers.get(ACL_ORIGIN), None)
self.assertEqual(resp.data.decode('utf-8'), u'')

def test_with_non_client_error_status(self):
''' If `invalid_cors_status_code` options set as 200,
which is not Clinet Error Status Code, response
code OK(200) will be returned when invalid CORS
request.
'''
for index, resp in enumerate(self.iter_responses(
'/with_non_client_error_response',
verbs=['get', 'head', 'options'],
origin='http://valid.com'
)):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), 'http://valid.com')
expected = u'WITH 304!' if index == 0 else u''
self.assertEqual(resp.data.decode('utf-8'), expected)

for resp in self.iter_responses('/with_non_client_error_response', origin='http://invalid.com'):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.headers.get(ACL_ORIGIN), None)
self.assertEqual(resp.data.decode('utf-8'), u'')


if __name__ == "__main__":
unittest.main()