Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cors): disallow cors_enable & additional CORSMiddleware combo #2201

Merged
merged 3 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/_newsfragments/1977.breakingchange.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Previously, it was possible to create an :class:`~falcon.App` with the
``cors_enable`` option, and add additional :class:`~falcon.CORSMiddleware`,
leading to unexpected behavior and dysfunctional CORS. This combination now
explicitly results in a :class:`ValueError`.
12 changes: 9 additions & 3 deletions docs/api/cors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,15 @@ Usage
allow_origins='example.com', allow_credentials='*'))

.. note::
Passing the ``cors_enable`` parameter set to ``True`` should be seen as
mutually exclusive with directly passing an instance of
:class:`~falcon.CORSMiddleware` to the application's initializer.
Passing the ``cors_enable`` parameter set to ``True`` is mutually exclusive
with directly passing an instance of :class:`~falcon.CORSMiddleware` to the
application's initializer.

.. versionchanged:: 4.0

Attempt to use the combination of ``cors_enable=True`` and an additional
instance of :class:`~falcon.CORSMiddleware` now results in a
:class:`ValueError`.

CORSMiddleware
--------------
Expand Down
25 changes: 22 additions & 3 deletions falcon/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
cors_enable=False,
sink_before_static_route=True,
):
self._cors_enable = cors_enable
self._sink_before_static_route = sink_before_static_route
self._sinks = []
self._static_routes = []
Expand Down Expand Up @@ -447,7 +448,7 @@
def router_options(self):
return self._router.options

def add_middleware(self, middleware: object) -> None:
def add_middleware(self, middleware: Union[object, Iterable]) -> None:
"""Add one or more additional middleware components.

Arguments:
Expand All @@ -461,10 +462,28 @@
# the chance that middleware may be None.
if middleware:
try:
self._unprepared_middleware += middleware
middleware = list(middleware) # type: ignore
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bonus points if anyone knows how type this correctly. Since we optionally allow passing in any object, mypy complains:

No overload variant of "list" matches argument type "object"  [call-overload]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you can. Maybe you can use

try:
  if TYPE_CHECKING:
    assert isinstance(middleware, Iterable)
  ...

But I'm not sure it's worth it

except TypeError:
# middleware is not iterable; assume it is just one bare component
self._unprepared_middleware.append(middleware)
middleware = [middleware]

if (
self._cors_enable
and len(
CaselIT marked this conversation as resolved.
Show resolved Hide resolved
[
mc
for mc in self._unprepared_middleware + middleware
if isinstance(mc, CORSMiddleware)
]
)
> 1
):
raise ValueError(

Check warning on line 481 in falcon/app.py

View check run for this annotation

Codecov / codecov/patch

falcon/app.py#L481

Added line #L481 was not covered by tests
'CORSMiddleware is not allowed in conjunction with '
'cors_enable (which already constructs one instance)'
)

self._unprepared_middleware += middleware

# NOTE(kgriffs): Even if middleware is None or an empty list, we still
# need to make sure self._middleware is initialized if this is the
Expand Down
30 changes: 19 additions & 11 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,20 +1000,28 @@ def test_process_resource_cached(self, asgi, independent_middleware):

class TestCORSMiddlewareWithAnotherMiddleware(TestMiddleware):
@pytest.mark.parametrize(
'mw',
'mw,allowed',
[
CaptureResponseMiddleware(),
[CaptureResponseMiddleware()],
(CaptureResponseMiddleware(),),
iter([CaptureResponseMiddleware()]),
(CaptureResponseMiddleware(), True),
([CaptureResponseMiddleware()], True),
((CaptureResponseMiddleware(),), True),
(iter([CaptureResponseMiddleware()]), True),
(falcon.CORSMiddleware(), False),
([falcon.CORSMiddleware()], False),
],
)
def test_api_initialization_with_cors_enabled_and_middleware_param(self, mw, asgi):
app = create_app(asgi, middleware=mw, cors_enable=True)
app.add_route('/', TestCorsResource())
client = testing.TestClient(app)
result = client.simulate_get(headers={'Origin': 'localhost'})
assert result.headers['Access-Control-Allow-Origin'] == '*'
def test_api_initialization_with_cors_enabled_and_middleware_param(
self, mw, asgi, allowed
):
if allowed:
app = create_app(asgi, middleware=mw, cors_enable=True)
app.add_route('/', TestCorsResource())
client = testing.TestClient(app)
result = client.simulate_get(headers={'Origin': 'localhost'})
assert result.headers['Access-Control-Allow-Origin'] == '*'
else:
with pytest.raises(ValueError, match='CORSMiddleware'):
app = create_app(asgi, middleware=mw, cors_enable=True)


@pytest.mark.skipif(cython, reason='Cythonized coroutine functions cannot be detected')
Expand Down