fix(upload): tipo da magic bytes, non da Content-Type dichiarato (#36 punto 2)

Il Content-Type del client non era verificato: un file "x.html" dichiarato
image/png veniva salvato e poi servito per estensione come text/html,
eseguito dal browser. Nuova detect_file_type() (PNG/JPEG/GIF/WebP/PDF) in
files.py, usata anche dal logo in settings.py; l'estensione salvata segue
sempre il tipo rilevato, mai il nome file dichiarato dal client. Il logo
rifiuta SVG a prescindere (puo' portare script). Upload letti a blocchi da
1MB, interrotti appena superano il limite invece di bufferizzare tutto
prima di controllare la dimensione.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:48:52 +02:00
parent bfe66a5710
commit 805f41c757
4 changed files with 219 additions and 41 deletions
+45
View File
@@ -65,6 +65,51 @@ class TestUploadFile:
assert data["file_type"] == "image/jpeg"
assert data["file_size"] > 0
async def test_extension_forced_to_detected_type_ignores_filename(
self, client: AsyncClient, maker_user: User, tmp_path: Path
):
"""gitea#36 point 2: the stored extension follows the detected magic
bytes, not the client-supplied filename — even a misleading one."""
jpeg_bytes = b"\xff\xd8\xff\xe0\x00\x10JFIF" + b"\x00" * 20 + b"\xff\xd9"
with patch("src.backend.api.routers.files.settings") as mock_settings:
mock_settings.upload_path = tmp_path
mock_settings.max_upload_size_mb = 50
resp = await client.post(
"/api/files/upload",
headers=auth_headers(maker_user),
# Filename claims .txt; content is a real JPEG.
files={"file": ("photo.txt", io.BytesIO(jpeg_bytes), "text/plain")},
)
assert resp.status_code == 200
data = resp.json()
assert data["file_type"] == "image/jpeg"
assert data["file_path"].endswith(".jpg")
async def test_upload_declared_type_lies_about_html_content(
self, client: AsyncClient, maker_user: User, tmp_path: Path
):
"""gitea#36 point 2: a file whose bytes are HTML must be rejected even
when the client declares Content-Type: image/png and names it .png —
this is what previously let a stored file be served back as
text/html by extension and executed by the browser."""
html_bytes = b"<html><body><script>alert(1)</script></body></html>"
with patch("src.backend.api.routers.files.settings") as mock_settings:
mock_settings.upload_path = tmp_path
mock_settings.max_upload_size_mb = 50
resp = await client.post(
"/api/files/upload",
headers=auth_headers(maker_user),
files={"file": ("x.png", io.BytesIO(html_bytes), "image/png")},
)
assert resp.status_code == 400
assert not any(tmp_path.rglob("x.png"))
async def test_upload_invalid_type(
self, client: AsyncClient, maker_user: User
):