fix(security): path traversal sur /download et /cleanup, validation UUID session_id et chemin input_path dans reconstruct-document
Made-with: Cursor
This commit is contained in:
@@ -20,6 +20,26 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(prefix="/api/v1", tags=["Legacy"])
|
router = APIRouter(prefix="/api/v1", tags=["Legacy"])
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_output_path(filename: str):
|
||||||
|
"""
|
||||||
|
Resolve filename to a path under config.OUTPUT_DIR. Prevents path traversal.
|
||||||
|
Returns (Path, True) if valid, (None, False) if invalid.
|
||||||
|
"""
|
||||||
|
if not filename or ".." in filename or "/" in filename or "\\" in filename:
|
||||||
|
return None, False
|
||||||
|
safe_name = Path(filename).name
|
||||||
|
if not safe_name.strip():
|
||||||
|
return None, False
|
||||||
|
base = config.OUTPUT_DIR.resolve()
|
||||||
|
try:
|
||||||
|
resolved = (config.OUTPUT_DIR / safe_name).resolve()
|
||||||
|
if not resolved.is_relative_to(base):
|
||||||
|
return None, False
|
||||||
|
return resolved, True
|
||||||
|
except (ValueError, OSError):
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
def _resolve_model(
|
def _resolve_model(
|
||||||
cfg_model: Optional[str],
|
cfg_model: Optional[str],
|
||||||
model_env: str,
|
model_env: str,
|
||||||
@@ -298,32 +318,30 @@ async def translate_batch_documents(
|
|||||||
|
|
||||||
@router.get("/download/{filename}")
|
@router.get("/download/{filename}")
|
||||||
async def download_file(filename: str):
|
async def download_file(filename: str):
|
||||||
"""Download a translated file by filename"""
|
"""Download a translated file by filename. Filename is sanitized to prevent path traversal."""
|
||||||
file_path = config.OUTPUT_DIR / filename
|
file_path, ok = _safe_output_path(filename)
|
||||||
|
if not ok or file_path is None:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=file_path,
|
path=file_path,
|
||||||
filename=filename,
|
filename=file_path.name,
|
||||||
media_type="application/octet-stream",
|
media_type="application/octet-stream",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/cleanup/{filename}")
|
@router.delete("/cleanup/{filename}")
|
||||||
async def cleanup_translated_file(filename: str):
|
async def cleanup_translated_file(filename: str):
|
||||||
"""Cleanup a translated file after download"""
|
"""Cleanup a translated file after download. Filename is sanitized to prevent path traversal."""
|
||||||
|
file_path, ok = _safe_output_path(filename)
|
||||||
|
if not ok or file_path is None:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||||
try:
|
try:
|
||||||
file_path = config.OUTPUT_DIR / filename
|
|
||||||
|
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
file_handler.cleanup_file(file_path)
|
file_handler.cleanup_file(file_path)
|
||||||
|
return {"message": f"File {file_path.name} deleted successfully"}
|
||||||
return {"message": f"File {filename} deleted successfully"}
|
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -454,8 +472,14 @@ async def reconstruct_document(
|
|||||||
),
|
),
|
||||||
target_language: str = Form(..., description="Target language code"),
|
target_language: str = Form(..., description="Target language code"),
|
||||||
):
|
):
|
||||||
"""Reconstruct a document with translated texts"""
|
"""Reconstruct a document with translated texts. session_id must be a valid UUID."""
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
uuid.UUID(session_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid session ID")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session_file = config.UPLOAD_DIR / f"session_{session_id}.json"
|
session_file = config.UPLOAD_DIR / f"session_{session_id}.json"
|
||||||
@@ -465,7 +489,10 @@ async def reconstruct_document(
|
|||||||
with open(session_file, "r", encoding="utf-8") as f:
|
with open(session_file, "r", encoding="utf-8") as f:
|
||||||
session_data = json.load(f)
|
session_data = json.load(f)
|
||||||
|
|
||||||
input_path = Path(session_data["input_path"])
|
input_path = Path(session_data["input_path"]).resolve()
|
||||||
|
upload_dir_resolved = config.UPLOAD_DIR.resolve()
|
||||||
|
if not input_path.is_relative_to(upload_dir_resolved):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid session data")
|
||||||
file_extension = session_data["file_extension"]
|
file_extension = session_data["file_extension"]
|
||||||
original_filename = session_data["original_filename"]
|
original_filename = session_data["original_filename"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user