mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-05 20:52:22 +01:00
fix(editor-drafts): refuse oversized drafts before parsing the body
The 256 MiB ceiling was only enforced inside _dump_payload, which runs after FastAPI has parsed the request and after json.dumps has re-serialised it. By that point the payload has been materialised several times over, so the limit rejected an allocation it had already paid for. Check Content-Length first, on both write routes. A request that omits or lies about the header still reaches the exact byte count, which remains authoritative.
This commit is contained in:
@@ -21,7 +21,7 @@ import logging
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from core.database import EditorDraft, SessionLocal
|
from core.database import EditorDraft, SessionLocal
|
||||||
@@ -76,13 +76,37 @@ def _load_payload(raw: Optional[str]) -> Dict[str, Any]:
|
|||||||
return payload if isinstance(payload, dict) else {}
|
return payload if isinstance(payload, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _draft_too_large() -> HTTPException:
|
||||||
|
return HTTPException(
|
||||||
|
413,
|
||||||
|
f"Editor draft exceeds the {EDITOR_DRAFT_MAX_BYTES // (1024 * 1024)} MB safety limit",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reject_oversized_draft_body(request: Request) -> None:
|
||||||
|
"""Refuse an oversized draft on ``Content-Length``, before the body is read.
|
||||||
|
|
||||||
|
``_dump_payload`` still owns the authoritative byte count, but it only runs
|
||||||
|
after the request has been parsed and re-serialised — by then the payload
|
||||||
|
has already been materialised several times over. Declaring a body past the
|
||||||
|
ceiling is enough to reject it, so do that first and cheaply. A request that
|
||||||
|
lies about or omits the header still hits the exact check further down.
|
||||||
|
"""
|
||||||
|
raw_length = request.headers.get("content-length")
|
||||||
|
if not raw_length:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
declared = int(raw_length)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return
|
||||||
|
if declared > EDITOR_DRAFT_MAX_BYTES:
|
||||||
|
raise _draft_too_large()
|
||||||
|
|
||||||
|
|
||||||
def _dump_payload(payload: Dict[str, Any]) -> str:
|
def _dump_payload(payload: Dict[str, Any]) -> str:
|
||||||
raw = json.dumps(payload or {}, separators=(",", ":"))
|
raw = json.dumps(payload or {}, separators=(",", ":"))
|
||||||
if len(raw.encode("utf-8")) > EDITOR_DRAFT_MAX_BYTES:
|
if len(raw.encode("utf-8")) > EDITOR_DRAFT_MAX_BYTES:
|
||||||
raise HTTPException(
|
raise _draft_too_large()
|
||||||
413,
|
|
||||||
f"Editor draft exceeds the {EDITOR_DRAFT_MAX_BYTES // (1024 * 1024)} MB safety limit",
|
|
||||||
)
|
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|
||||||
@@ -120,7 +144,11 @@ def setup_editor_draft_routes() -> APIRouter:
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@router.post("/api/editor-drafts")
|
@router.post("/api/editor-drafts")
|
||||||
async def create_draft(request: Request, body: DraftCreate) -> Dict[str, Any]:
|
async def create_draft(
|
||||||
|
request: Request,
|
||||||
|
body: DraftCreate,
|
||||||
|
_size_guard: None = Depends(reject_oversized_draft_body),
|
||||||
|
) -> Dict[str, Any]:
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
@@ -148,7 +176,12 @@ def setup_editor_draft_routes() -> APIRouter:
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@router.put("/api/editor-drafts/{draft_id}")
|
@router.put("/api/editor-drafts/{draft_id}")
|
||||||
async def update_draft(request: Request, draft_id: str, body: DraftUpdate) -> Dict[str, Any]:
|
async def update_draft(
|
||||||
|
request: Request,
|
||||||
|
draft_id: str,
|
||||||
|
body: DraftUpdate,
|
||||||
|
_size_guard: None = Depends(reject_oversized_draft_body),
|
||||||
|
) -> Dict[str, Any]:
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user