diff --git a/backend/app/main.py b/backend/app/main.py index 2727e5e..2cf85d0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -691,7 +691,14 @@ async def scan( status_code=413, detail=f"Header indicates file is too large. Maximum upload size is {MAX_UPLOAD_MB}MB.", ) + filename = project.filename or "" + if not filename.lower().endswith(".zip"): + raise HTTPException( + status_code=415, + detail="Only .zip archives are accepted. Please compress your project and try again.", + ) + job_id = next(tempfile._get_candidate_names()) job_dir = WORK_ROOT / job_id ensure_dir(job_dir) diff --git a/backend/tests/test_upload_security.py b/backend/tests/test_upload_security.py index 92725ee..81fbd70 100644 --- a/backend/tests/test_upload_security.py +++ b/backend/tests/test_upload_security.py @@ -44,3 +44,20 @@ def test_upload_aborts_when_stream_exceeds_limit(): assert response.status_code == 413 assert "Actual file size exceeds" in response.json()["detail"] +def test_upload_rejects_non_zip_file(): + """ + Test that the server immediately rejects uploads whose filename + does not end with .zip. + """ + files = { + "project": ("test.txt", io.BytesIO(b"hello"), "text/plain") + } + data = {"project": "test_project"} + + response = client.post("/scan", files=files, data=data) + + assert response.status_code == 415 + assert ( + response.json()["detail"] + == "Only .zip archives are accepted. Please compress your project and try again." + ) \ No newline at end of file