From 1e3d9968b2aca3a9a138f62004b1a4ef04ebc2f4 Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Sun, 12 Apr 2026 12:47:31 -0500 Subject: [PATCH 1/4] feat: reject requests with body larger than 1MB FastAPI/Starlette has no default body size limit. Add a middleware that returns 413 when Content-Length exceeds 1MB, protecting against unbounded JSON payload DoS. --- app/main.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/main.py b/app/main.py index d1db77b..1b23fd3 100644 --- a/app/main.py +++ b/app/main.py @@ -48,6 +48,20 @@ app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +MAX_REQUEST_BODY_SIZE = 1_048_576 # 1 MB + + +@app.middleware("http") +async def limit_request_body_size(request: Request, call_next) -> Response: + content_length = request.headers.get("content-length") + if content_length and int(content_length) > MAX_REQUEST_BODY_SIZE: + return JSONResponse( + status_code=413, + content={"detail": "Request body too large"}, + ) + return await call_next(request) + + # --- Auth routes --- # Custom refresh/logout routes (included before FastAPI-Users so /auth/jwt/logout is shadowed) app.include_router(auth_refresh_router) From ae4bd24a18c95e8fd3180dc1d441138d04dcb88a Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Sun, 12 Apr 2026 12:47:39 -0500 Subject: [PATCH 2/4] feat(logging): bind user_id to structlog contextvars on auth Wrap fastapi-users current_active_user and current_superuser so every log emitted during an authenticated request automatically carries user_id alongside request_id. No caller changes required. --- app/auth/users.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/auth/users.py b/app/auth/users.py index d1ec519..5f48b6b 100644 --- a/app/auth/users.py +++ b/app/auth/users.py @@ -81,5 +81,19 @@ async def get_user_manager( fastapi_users = FastAPIUsers[User, UUID](get_user_manager, [auth_backend]) -current_active_user = fastapi_users.current_user(active=True) -current_superuser = fastapi_users.current_user(active=True, superuser=True) +_fastapi_users_current_active = fastapi_users.current_user(active=True) +_fastapi_users_current_superuser = fastapi_users.current_user(active=True, superuser=True) + + +async def current_active_user( + user: User = Depends(_fastapi_users_current_active), +) -> User: + structlog.contextvars.bind_contextvars(user_id=str(user.id)) + return user + + +async def current_superuser( + user: User = Depends(_fastapi_users_current_superuser), +) -> User: + structlog.contextvars.bind_contextvars(user_id=str(user.id)) + return user From eb2583f10dea0e516deda52e013c848866a1513f Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Sun, 12 Apr 2026 12:47:44 -0500 Subject: [PATCH 3/4] feat(schemas): add max_length bounds to user-supplied string fields Demonstrates the Field(max_length=...) pattern on NoteBase.body (10k chars) and UserCreate/UserUpdate.name (100 chars). Unbounded string inputs allow malicious clients to store arbitrarily large values. Response schemas (UserRead) are left unbounded since the data originates from our own DB. --- app/schemas/note.py | 8 ++++++-- app/schemas/user.py | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/schemas/note.py b/app/schemas/note.py index 95c1333..1524547 100644 --- a/app/schemas/note.py +++ b/app/schemas/note.py @@ -8,7 +8,11 @@ class NoteBase(BaseModel): """Base schema with shared note fields.""" title: str = Field(..., min_length=1, max_length=200, examples=["Meeting notes"]) - body: str | None = Field(None, examples=["Discussed project timeline and milestones."]) + body: str | None = Field( + None, + max_length=10_000, + examples=["Discussed project timeline and milestones."], + ) class NoteCreate(NoteBase): @@ -21,7 +25,7 @@ class NoteUpdate(BaseModel): """Schema for updating a note. All fields optional.""" title: str | None = Field(None, min_length=1, max_length=200) - body: str | None = None + body: str | None = Field(None, max_length=10_000) class NoteRead(NoteBase): diff --git a/app/schemas/user.py b/app/schemas/user.py index e05b22e..914041a 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -1,6 +1,7 @@ from uuid import UUID from fastapi_users import schemas +from pydantic import Field class UserRead(schemas.BaseUser[UUID]): @@ -13,10 +14,10 @@ class UserRead(schemas.BaseUser[UUID]): class UserCreate(schemas.BaseUserCreate): """Schema for creating a new user.""" - name: str | None = None + name: str | None = Field(None, max_length=100) class UserUpdate(schemas.BaseUserUpdate): """Schema for updating user data.""" - name: str | None = None + name: str | None = Field(None, max_length=100) From 4bb9766a3fd85ab774529c88e566762ec5dfb8e0 Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Sun, 12 Apr 2026 12:47:49 -0500 Subject: [PATCH 4/4] chore: add CI contents:read permission and CODEOWNERS - CI: restrict GITHUB_TOKEN to contents:read (least privilege) - .github/CODEOWNERS catch-all placeholder for review enforcement --- .github/CODEOWNERS | 1 + .github/workflows/ci.yml | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..9a9a2df --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @amrtgaber diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de9d4d3..2b16138 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest