resolved RESOLVED UNSUBMITTED PASS · None tool calls · 0 s · fastapi/fastapi
♻️ Refactor logic to handle OpenAPI and Swagger UI escaping data ♻️ Refactor logic to handle OpenAPI and Swagger UI escaping data OpenAPI, do not store `root_path` in servers: the only way this could be a problem is if there was a misconfigured proxy that somehow allowed an attacker client to set `x-forwarded-*` headers and passed them along. For a proxy (or server) to do this, it normally has to be intentionally/explicitly misconfigured. But again, doesn't hurt to have it there. Escape Swagger UI configs: I wouldn't consider this really important, the Swagger UI logic takes only data from the same developer building the app, I don't see a feasible scenario where this could be a problem, but probably also doesn't hurt much to have it there. --- I received several "security reports" with this, I suspect some automated scanning tool that checks any JSON inside of HTML or similar. I don't consider these security issues, but also think it's probably fine to have these changes.
| # | Tool | Arguments | Result |
|---|---|---|---|
| No trace captured. | |||
--- a/fastapi/applications.py
+++ b/fastapi/applications.py
@@ -1101,16 +1101,18 @@ def openapi(self) -> dict[str, Any]:
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:
--- a/fastapi/openapi/docs.py
+++ b/fastapi/openapi/docs.py
@@ -5,6 +5,20 @@
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 <script> tag.
+ """
+ return (
+ json.dumps(value)
+ .replace("<", "\\u003c")
+ .replace(">", "\\u003e")
+ .replace("&", "\\u0026")
+ )
+
+
swagger_ui_default_parameters: Annotated[
dict[str, Any],
Doc(
@@ -155,7 +169,7 @@ def get_swagger_ui_html(
"""
for key, value in current_swagger_ui_parameters.items():
- html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n"
+ html += f"{_html_safe_json(key)}: {_html_safe_json(jsonable_encoder(value))},\n"
if oauth2_redirect_url:
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
@@ -169,7 +183,7 @@ def get_swagger_ui_html(
if init_oauth:
html += f"""
- ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
+ ui.initOAuth({_html_safe_json(jsonable_encoder(init_oauth))})
"""
html += """....... [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.52.1-py3-none-any/starlette/testclient.py:45
/Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.52.1-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
7 passed, 1 warning in 0.45s