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

Add request_max_size option #2175

Closed

Conversation

alex-oleshkevich
Copy link
Member

This PR adds an option to limit request body size. This is a mitigation of some DoS attacks.

How it works
There are two places where you can define the limit: app instance and route. When defined on the app, the limit becomes global and guards all endpoints. The developer still can overwrite this value on per-route bases (for example on file upload routes).

app = Starlette(
    request_max_size=5,  # global
    routes=[Route("/", endpoint=read_view, methods=["POST"], request_max_size=24)], # route level
)

response = client.post("/", data=SOME_LARGE_PAYLOAD) 
assert response.status_code == 413

Refs #2155

@alex-oleshkevich
Copy link
Member Author

This can be a base for the next guard - request timeout.

@alex-oleshkevich
Copy link
Member Author

It is still possible to read unlimited data in mounted apps and in the middleware. But, I think, it is a developer's responsibility in this case.

@@ -61,6 +61,7 @@ def __init__(
on_startup: typing.Optional[typing.Sequence[typing.Callable]] = None,
on_shutdown: typing.Optional[typing.Sequence[typing.Callable]] = None,
lifespan: typing.Optional[Lifespan["AppType"]] = None,
request_max_size: int = 2621440, # 2.5mb
Copy link
Member Author

Choose a reason for hiding this comment

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

Like in Django

Copy link
Member

Choose a reason for hiding this comment

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

Can you permalink where you got this from in a comment below here just for the record? I'd also be okay with a code comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

What does flask do?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

body = message.get("body", b"")
bytes_read += len(body)
if bytes_read > max_body_size:
raise HTTPException(
Copy link
Member Author

@alex-oleshkevich alex-oleshkevich Jun 6, 2023

Choose a reason for hiding this comment

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

Is the exception good enough?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah fine by me

Copy link
Member

Choose a reason for hiding this comment

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

Please follow the same logic as above.

        if "app" in scope:
            raise HTTPException(status_code=413, "Request Entity Too Large")
        else:
            response = PlainTextResponse("Request Entity Too Large", status_code=404)

@Kludex Kludex requested a review from tomchristie June 6, 2023 10:19
@@ -78,6 +79,7 @@ def __init__(
)
self.user_middleware = [] if middleware is None else list(middleware)
self.middleware_stack: typing.Optional[ASGIApp] = None
self.request_max_size = request_max_size
Copy link
Member

Choose a reason for hiding this comment

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

It might be a good idea to start making these attributes private.

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently, all members of Starlette are public. I would not like to introduce a new style here.

Copy link
Member

Choose a reason for hiding this comment

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

Not true:

self._url = url

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Fair enough. I still think a lot of those shouldn't have been public in the first place and just because they are doesn't mean we should make this public as well.

starlette/applications.py Outdated Show resolved Hide resolved
starlette/routing.py Outdated Show resolved Hide resolved
body = message.get("body", b"")
bytes_read += len(body)
if bytes_read > max_body_size:
raise HTTPException(
Copy link
Member

Choose a reason for hiding this comment

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

Yeah fine by me

@Kludex Kludex added this to the Version 1.x milestone Jun 20, 2023
@adriangb
Copy link
Member

A couple things to consider @alex-oleshkevich:

  • Can we make the configurator a middleware? That way it can be applied to Mount, etc. It makes sense to keep the parameters to the application object since I imagine a global and a per route override like you laid out here is going to be the most common approach, but it’d be nice to make it a bit more flexible. Also so that it can be used with current versions of FastAPI.
  • Can we make the per-route enforcement a middleware that gets applied automatically? My main reasoning for this is to be able to use it in existing FastAPI applications, but I imagine other use cases like enforcing at the router level or on a mounted app may make sense

@alex-oleshkevich
Copy link
Member Author

It can be a middleware from #2328 that automatically gets applied globally and per-route. And be reused anywhere what can accept a middleware (like Mount).

@adriangb
Copy link
Member

That sounds good to me. It’ll be two middleware’s right? One to set the values (or override them), and another to enforce them.

@alex-oleshkevich
Copy link
Member Author

Yes, like that. One middleware in the Starlette.middleware_stack and another one in the Route.handle. However, I am aware that middleware may add a method-call overhead compared to middlewareless implementation.

@alex-oleshkevich
Copy link
Member Author

It may be worth to have a shared code to use in app, route and middleware.

@adriangb
Copy link
Member

Yeah I was thinking we make two middlewares, let's call them RequestSizeLimitConfigurationMiddleware and RequestSizeLimitEnforcementMiddleware. In Starlette we'd do something like:

def __init__(self, ..., request_max_size_bytes: int | None = 1234) -> None:
    if request_max_size_bytes:
        self.app = RequestSizeLimitConfigurationMiddleware(self.app, request_max_size_bytes=request_max_size_bytes)

And then in Route (or maybe rather in the request_response function as pointed out above) we'd do something like:

def request_response(
    func: typing.Callable[[Request], typing.Union[typing.Awaitable[Response], Response]],
    max_request_size: int | None = 1234,
) -> ASGIApp:
    """
    Takes a function or coroutine `func(request) -> response`,
    and returns an ASGI application.
    """

    async def app(scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope, receive, send)

        async def app(scope: Scope, receive: Receive, send: Send) -> None:
            if is_async_callable(func):
                response = await func(request)
            else:
                response = await run_in_threadpool(func, request)
            await response(scope, receive, send)

        await wrap_app_handling_exceptions(app, request)(scope, receive, send)
    if max_request_size is not None:
        app = RequestSizeLimitEnforcementMiddleware(app, max_request_size=max_request_size)
    return app

@alex-oleshkevich
Copy link
Member Author

@Kludex what should we do with this ticket? It is stale and can be closed.

@Kludex
Copy link
Member

Kludex commented Jan 20, 2024

@Kludex what should we do with this ticket? It is stale and can be closed.

Let's close this, and proceed with the subject on the issue, or on #2328.

Thanks Alex.

@Kludex Kludex closed this Jan 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants