Security fix and test reorganization

- Add sanitize_filename() to prevent Content-Disposition header injection
- Remove unused imports from models.py and artifact_cleanup.py
- Reorganize tests into unit/ and integration/ structure
- Add factories.py for test data generation
- Split old test files into focused test modules (143 tests)
This commit is contained in:
Mondo Diaz
2026-01-06 15:04:51 -06:00
parent a293432d2e
commit b81c69118f
20 changed files with 3007 additions and 2626 deletions

View File

@@ -95,6 +95,18 @@ from .config import get_settings
router = APIRouter()
def sanitize_filename(filename: str) -> str:
"""Sanitize filename for use in Content-Disposition header.
Removes characters that could enable header injection attacks:
- Double quotes (") - could break out of quoted filename
- Carriage return (\\r) and newline (\\n) - could inject headers
"""
import re
return re.sub(r'[\r\n"]', "", filename)
def get_user_id(request: Request) -> str:
"""Extract user ID from request (simplified for now)"""
api_key = request.headers.get("X-Orchard-API-Key")
@@ -1553,7 +1565,7 @@ def download_artifact(
if not artifact:
raise HTTPException(status_code=404, detail="Artifact not found")
filename = artifact.original_name or f"{artifact.id}"
filename = sanitize_filename(artifact.original_name or f"{artifact.id}")
# Determine download mode (query param overrides server default)
download_mode = mode or settings.download_mode
@@ -1666,7 +1678,7 @@ def get_artifact_url(
if not artifact:
raise HTTPException(status_code=404, detail="Artifact not found")
filename = artifact.original_name or f"{artifact.id}"
filename = sanitize_filename(artifact.original_name or f"{artifact.id}")
url_expiry = expiry or settings.presigned_url_expiry
presigned_url = storage.generate_presigned_url(
@@ -1717,7 +1729,7 @@ def head_artifact(
if not artifact:
raise HTTPException(status_code=404, detail="Artifact not found")
filename = artifact.original_name or f"{artifact.id}"
filename = sanitize_filename(artifact.original_name or f"{artifact.id}")
return Response(
content=b"",