From 06ec088a5622d4c4f40a0809255fabc07483119c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 12 May 2026 05:19:39 +0000 Subject: [PATCH] studio: enforce body cap on chunked uploads and drop unsafe-inline from script-src MaxBodyMiddleware previously only inspected the declared Content-Length header; clients omitting it or sending Transfer-Encoding: chunked bypassed the cap and could still drive an OOM via the downstream JSON / file readers on /v1/chat/completions, /api/inference, /api/data-recipe, /api/datasets, /api/train, /api/export. Rewrite as a raw ASGI middleware that drains and counts http.request frames, replies 413 once the running total exceeds UNSLOTH_STUDIO_MAX_BODY_MB before invoking the FastAPI handler, and replays the buffered body to downstream so route code that calls request.json() / await request.body() works unchanged. CSP previously included 'unsafe-inline' on script-src, which defeats the main XSS protection. The frontend bundle does not need inline scripts; the only inline " + nonce = _secrets.token_urlsafe(16) + tag = f'' html = html_bytes.decode("utf-8") html = html.replace("", f"{tag}", 1) - return html.encode("utf-8") + return html.encode("utf-8"), nonce def setup_frontend(app: FastAPI, build_path: Path): @@ -599,17 +715,23 @@ def setup_frontend(app: FastAPI, build_path: Path): if assets_dir.exists(): app.mount("/assets", StaticFiles(directory = assets_dir), name = "assets") - @app.get("/") - async def serve_root(): + def _build_index_response() -> Response: content = (build_path / "index.html").read_bytes() content = _strip_crossorigin(content) - content = _inject_bootstrap(content, app) + content, nonce = _inject_bootstrap(content, app) + headers = {"Cache-Control": "no-cache, no-store, must-revalidate"} + if nonce: + headers[_CSP_SCRIPT_NONCE_HEADER] = nonce return Response( content = content, media_type = "text/html", - headers = {"Cache-Control": "no-cache, no-store, must-revalidate"}, + headers = headers, ) + @app.get("/") + async def serve_root(): + return _build_index_response() + @app.get("/{full_path:path}") async def serve_frontend(full_path: str): if full_path in {"api", "v1"} or full_path.startswith(("api/", "v1/")): @@ -625,13 +747,6 @@ def setup_frontend(app: FastAPI, build_path: Path): return FileResponse(file_path) # Serve index.html as bytes — avoids Content-Length mismatch - content = (build_path / "index.html").read_bytes() - content = _strip_crossorigin(content) - content = _inject_bootstrap(content, app) - return Response( - content = content, - media_type = "text/html", - headers = {"Cache-Control": "no-cache, no-store, must-revalidate"}, - ) + return _build_index_response() return True