From 2686c7fbbf9abb17902a2981a9d9fca01f5117b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Feb 2026 01:28:10 -0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20logic=20to=20ha?= =?UTF-8?q?ndle=20OpenAPI=20and=20Swagger=20UI=20escaping=20data=20(#14986?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 16 +++--- fastapi/openapi/docs.py | 18 ++++++- tests/test_openapi_cache_root_path.py | 75 +++++++++++++++++++++++++++ tests/test_swagger_ui_escape.py | 37 +++++++++++++ 4 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 tests/test_openapi_cache_root_path.py create mode 100644 tests/test_swagger_ui_escape.py diff --git a/fastapi/applications.py b/fastapi/applications.py index ed05a1ff9e..e7e816c2e9 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1101,16 +1101,18 @@ class FastAPI(Starlette): def setup(self) -> None: if self.openapi_url: - urls = (server_data.get("url") for server_data in self.servers) - server_urls = {url for url in urls if url} async def openapi(req: Request) -> JSONResponse: root_path = req.scope.get("root_path", "").rstrip("/") - if root_path not in server_urls: - if root_path and self.root_path_in_servers: - self.servers.insert(0, {"url": root_path}) - server_urls.add(root_path) - return JSONResponse(self.openapi()) + schema = self.openapi() + if root_path and self.root_path_in_servers: + server_urls = {s.get("url") for s in schema.get("servers", [])} + if root_path not in server_urls: + schema = dict(schema) + schema["servers"] = [{"url": root_path}] + schema.get( + "servers", [] + ) + return JSONResponse(schema) self.add_route(self.openapi_url, openapi, include_in_schema=False) if self.openapi_url and self.docs_url: diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index b845f87c1c..0d9242f9fa 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -5,6 +5,20 @@ from annotated_doc import Doc from fastapi.encoders import jsonable_encoder from starlette.responses import HTMLResponse + +def _html_safe_json(value: Any) -> str: + """Serialize a value to JSON with HTML special characters escaped. + + This prevents injection when the JSON is embedded inside a " + html = get_swagger_ui_html( + openapi_url="/openapi.json", + title="Test", + init_oauth={"appName": xss_payload}, + ) + body = html.body.decode() + + assert "