Pre-deploy security/correctness fixes (port log E17)
- Remove dead kitchen->KDS internal API (api/internal.py, verify_internal_secret) — KDS reads kitchen_db directly (E16), nothing ever called this endpoint - Add expires_at to dispute_attachments; public attachment links now expire after 30 days instead of staying valid forever (A4) - Add services/upload_validation.py: sniff real file content via python-magic instead of trusting the client-supplied Content-Type header, plus a 20MB cap. Applied across invoices/logbook/food_flags/credit_notes/disputes upload endpoints (A5) — disputes previously had no file-type check at all - Fix nginx client_max_body_size drift (800m -> the plan's intended 20m) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
78744278f8
commit
bcc94024e3
15 changed files with 124 additions and 109 deletions
40
backend/services/upload_validation.py
Normal file
40
backend/services/upload_validation.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""
|
||||
Shared upload validation — sniffs the real file content (libmagic) rather
|
||||
than trusting the client-supplied Content-Type header, and enforces a size
|
||||
cap. See port log A5: the archive only checked `file.content_type`, which is
|
||||
attacker-controlled and proves nothing about what's actually in the body.
|
||||
"""
|
||||
import magic
|
||||
from fastapi import HTTPException, UploadFile
|
||||
|
||||
DEFAULT_MAX_BYTES = 20 * 1024 * 1024 # 20 MB — matches nginx client_max_body_size
|
||||
|
||||
|
||||
async def read_and_validate_upload(
|
||||
file: UploadFile,
|
||||
allowed_mimes: set[str],
|
||||
max_bytes: int = DEFAULT_MAX_BYTES,
|
||||
) -> bytes:
|
||||
"""
|
||||
Read an UploadFile fully, verify its sniffed MIME type is in
|
||||
`allowed_mimes`, and enforce `max_bytes`. Returns the file bytes for the
|
||||
caller to save/process. Raises HTTPException(400) on any failure.
|
||||
"""
|
||||
content = await file.read()
|
||||
|
||||
if not content:
|
||||
raise HTTPException(status_code=400, detail="Empty file")
|
||||
if len(content) > max_bytes:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"File too large — max {max_bytes // (1024 * 1024)}MB",
|
||||
)
|
||||
|
||||
sniffed = magic.from_buffer(content, mime=True)
|
||||
if sniffed not in allowed_mimes:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"File content doesn't match an allowed type (detected: {sniffed})",
|
||||
)
|
||||
|
||||
return content
|
||||
Loading…
Add table
Add a link
Reference in a new issue