adriangb / xpresso

A composable Python ASGI web framework
https://xpresso-api.dev/
MIT License
176 stars 4 forks source link

DDoS prevention #119

Open adriangb opened 1 year ago

adriangb commented 1 year ago

I think most web frameworks should, by default, prevent DDoS and Slow Loris attacks. There's a bunch of places this needs to happen, here are some ideas:

With regards to form parsing, we currently rely entirely on Starlette and blindly call Request.form(), which is not great given we have a ton of metadata about what we expect in the form. Inspired by multer I would like something like this:

from typing import Annotated, List

import annotated_types as at
from xpresso.bodies import FormField, FormFile, Multipart

MB = 2**20

# Define a model, this restricts the fields that can be included
# unless extra = "allow" is set on the Pydantic model itself
# This protects against attacks that spam a lot of fields
class MyForm(BaseModel):
    # Restrict the age field to 4 bytes
    # If the client sends more than this we'll error immediately
    # We also use Pydantic to automatically parse the string into an int
    age: Annotated[int, FormField(), at.Len(max_length=10)]
    # Images is a list of bytes
    # We first declare that the list itself cannot have more than 10 items
    # Xpresso would have to check if the type is a List and if so check if `max_items` is set on the field
    # as a special case since we can use this to error before reading more fields
    # (I think re-using Pydantic's metadata makes sense when possible)
    # We also limit each image to 10 MB and will immediately stop parsing and error
    # if it is larger than this
    images: Annotated[List[Annotated[bytes, FormFile(), at.Len(max_length=10*MB], at.Len(max_length=10)]
    extra_data: Annotated[bytes, FormFile()]  # just to demonstrate max_length below

# restrict the size (in bytes) of the entire body to 100MB
async def endpoint(form: Annotated[MyForm, Multipart(max_length=100*MB)]):
    ...

This is a super explicit and perhaps overkill example, but it gets the idea across.

With JSON or raw bytes we can at least limit the total size of the request body:

async def endpoint(form: Annotated[SomeModel, Json(max_length=1*MB)]):
    ...
adriangb commented 1 year ago

And the idea would be that the web framework itself imposes some reasonable defaults.