From 2e06a00934bb3594c06df2d413e0a91c70146a0e Mon Sep 17 00:00:00 2001 From: Roland Tannous Date: Wed, 25 Feb 2026 01:05:04 +0000 Subject: [PATCH] fix: replace FileResponse with Response for index.html to prevent Content-Length mismatch and add path traversal guard --- studio/backend/main.py | 56 ++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/studio/backend/main.py b/studio/backend/main.py index 466be151d8..331962fb67 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -8,7 +8,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, HTMLResponse, Response from pathlib import Path from datetime import datetime @@ -156,26 +156,44 @@ async def get_hardware_info(): def setup_frontend(app: FastAPI, build_path: Path): """Mount frontend static files (optional)""" - if build_path.exists(): - # Mount assets - assets_dir = build_path / "assets" - if assets_dir.exists(): - app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + if not build_path.exists(): + return False - @app.get("/") - async def serve_root(): - return FileResponse(build_path / "index.html", headers={"Cache-Control": "no-cache, no-store, must-revalidate"}) + # Mount assets + assets_dir = build_path / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") - @app.get("/{full_path:path}") - async def serve_frontend(full_path: str): - if full_path.startswith("api"): - return {"error": "API endpoint not found"} + @app.get("/") + async def serve_root(): + content = (build_path / "index.html").read_bytes() + return Response( + content=content, + media_type="text/html", + headers={"Cache-Control": "no-cache, no-store, must-revalidate"}, + ) - file_path = build_path / full_path - if file_path.is_file(): - return FileResponse(file_path) + @app.get("/{full_path:path}") + async def serve_frontend(full_path: str): + if full_path.startswith("api"): + return {"error": "API endpoint not found"} - return FileResponse(build_path / "index.html", headers={"Cache-Control": "no-cache, no-store, must-revalidate"}) + file_path = (build_path / full_path).resolve() + + # Block path traversal — ensure resolved path stays inside build_path + if not str(file_path).startswith(str(build_path.resolve())): + return Response(status_code=403) + + if file_path.is_file(): + return FileResponse(file_path) + + # Serve index.html as bytes — avoids Content-Length mismatch + content = (build_path / "index.html").read_bytes() + return Response( + content=content, + media_type="text/html", + headers={"Cache-Control": "no-cache, no-store, must-revalidate"}, + ) + + return True - return True - return False