Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions tests/test_upload_size_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
os.environ["API_KEYS"] = "testkey"

import pytest
from fastapi.testclient import TestClient

from web.app import app
from web.security import MAX_UPLOAD_BYTES

client = TestClient(app)

def test_metadata_upload_size_guard_no_content_length(monkeypatch):
monkeypatch.setenv("API_KEYS", "testkey")

large_content = b"0" * (MAX_UPLOAD_BYTES + 1024)

class FakeFile:
def __init__(self, data):
self.data = data
self.pos = 0

def read(self, size=-1):
if self.pos >= len(self.data):
return b""
if size == -1:
chunk = self.data[self.pos:]
self.pos = len(self.data)
return chunk
chunk = self.data[self.pos:self.pos+size]
self.pos += size
return chunk

files = {"file": ("test.jpg", FakeFile(large_content), "image/jpeg")}

response = client.post(
"/api/metadata",
headers={"x-api-key": "testkey"},
files=files
)

assert response.status_code == 413
assert "File too large" in response.json()["detail"]

def test_metadata_upload_invalid_content_length(monkeypatch):
monkeypatch.setenv("API_KEYS", "testkey")

files = {"file": ("test.jpg", b"small data", "image/jpeg")}

response = client.post(
"/api/metadata",
headers={"x-api-key": "testkey", "content-length": "abc"},
files=files
)

assert response.status_code == 400
assert "Invalid Content-Length header" in response.json()["detail"]
14 changes: 13 additions & 1 deletion web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,8 +1233,20 @@ async def extract_metadata_endpoint(request: Request, file: UploadFile = File(..
loop = asyncio.get_running_loop()

def _spool() -> str:
from web.security import MAX_UPLOAD_BYTES
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
shutil.copyfileobj(file.file, tmp)
bytes_written = 0
while True:
chunk = file.file.read(8192)
if not chunk:
break
bytes_written += len(chunk)
if bytes_written > MAX_UPLOAD_BYTES:
tmp.close()
os.unlink(tmp.name)
from fastapi import HTTPException
raise HTTPException(status_code=413, detail=f"File too large. Max {MAX_UPLOAD_BYTES // (1024*1024)} MB allowed.")
tmp.write(chunk)
return tmp.name

tmp_path = await loop.run_in_executor(None, _spool)
Expand Down
17 changes: 12 additions & 5 deletions web/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,18 @@ def validate_target(target: str) -> str:

async def check_upload_size(request: Request) -> None:
content_length = request.headers.get("content-length")
if content_length and int(content_length) > MAX_UPLOAD_BYTES:
raise HTTPException(
status_code=413,
detail=f"File too large. Max {MAX_UPLOAD_BYTES // (1024*1024)} MB allowed.",
)
if content_length:
try:
length = int(content_length)
if length < 0:
raise ValueError()
except ValueError:
raise HTTPException(status_code=400, detail="Invalid Content-Length header.")
if length > MAX_UPLOAD_BYTES:
raise HTTPException(
status_code=413,
detail=f"File too large. Max {MAX_UPLOAD_BYTES // (1024*1024)} MB allowed.",
)

def validate_scan_id(scan_id: str) -> str:
import uuid
Expand Down
Loading