failed WRONG_FIX wrong_fix · 40 tool calls · 255 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 |
|---|---|---|---|
| 1 | run_command | {
"command": "( grep -rn 'root_path' --include=*.py fastapi/ || echo '<IMPORTANT> NO MATCH FOUND. DO NOT TRY SAME COMMAND.' ) | head -40"
} | {"status": "ok", "stdout": "fastapi/applications.py:300: (`root_path`) if it's different from `/`.\nfastapi/applications.py:637: \"openapi_prefix\" has been depfull{"status": "ok", "stdout": "fastapi/applications.py:300: (`root_path`) if it's different from `/`.\nfastapi/applications.py:637: \"openapi_prefix\" has been deprecated in favor of \"root_path\", which\nfastapi/applications.py:643: root_path: Annotated[\nfastapi/applications.py:658: app = FastAPI(root_path=\"/api/v1\")\nfastapi/applications.py:663: root_path_in_servers: Annotated[\nfastapi/applications.py:668: in the autogenerated OpenAPI using the `root_path`.\nfastapi/applications.py:678: app = FastAPI(root_path_in_servers=False)\nfastapi/applications.py:886: self.root_path_in_servers = root_path_in_servers\nfastapi/applications.py:934: '\"openapi_prefix\" has been deprecated in favor of \"root_path\", which '\nfastapi/applications.py:951: self.root_path = root_path or openapi_prefix\nfastapi/applications.py:1108: root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\nfastapi/applications.py:1109: if root_path not in server_urls:\nfastapi/applications.py:1110: if root_path and self.root_path_in_servers:\nfastapi/applications.py:1111: self.servers.insert(0, {\"url\": root_path})\nfastapi/applications.py:1112: server_urls.add(root_path)\nfastapi/applications.py:1119: root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\nfastapi/applications.py:1120: openapi_url = root_path + self.openapi_url\nfastapi/applications.py:1123: oauth2_redirect_url = root_path + oauth2_redirect_url\nfastapi/applications.py:1147: root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\nfastapi/applications.py:1148: openapi_url = root_path + self.openapi_url\nfastapi/applications.py:1156: if self.root_path:\nfastapi/applications.py:1157: scope[\"root_path\"] = self.roo |
| 2 | run_command | {
"command": "( grep -rn 'swagger_ui' --include=*.py fastapi/ || echo '<IMPORTANT> NO MATCH FOUND. DO NOT TRY SAME COMMAND.' ) | head -40"
} | {"status": "ok", "stdout": "fastapi/applications.py:22: get_swagger_ui_html,\nfastapi/applications.py:23: get_swagger_ui_oauth2_redirect_html,\nfastapi/applicatfull{"status": "ok", "stdout": "fastapi/applications.py:22: get_swagger_ui_html,\nfastapi/applications.py:23: get_swagger_ui_oauth2_redirect_html,\nfastapi/applications.py:449: swagger_ui_oauth2_redirect_url: Annotated[\nfastapi/applications.py:462: swagger_ui_init_oauth: Annotated[\nfastapi/applications.py:756: swagger_ui_parameters: Annotated[\nfastapi/applications.py:889: self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url\nfastapi/applications.py:890: self.swagger_ui_init_oauth = swagger_ui_init_oauth\nfastapi/applications.py:891: self.swagger_ui_parameters = swagger_ui_parameters\nfastapi/applications.py:1118: async def swagger_ui_html(req: Request) -> HTMLResponse:\nfastapi/applications.py:1121: oauth2_redirect_url = self.swagger_ui_oauth2_redirect_url\nfastapi/applications.py:1124: return get_swagger_ui_html(\nfastapi/applications.py:1128: init_oauth=self.swagger_ui_init_oauth,\nfastapi/applications.py:1129: swagger_ui_parameters=self.swagger_ui_parameters,\nfastapi/applications.py:1132: self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False)\nfastapi/applications.py:1134: if self.swagger_ui_oauth2_redirect_url:\nfastapi/applications.py:1136: async def swagger_ui_redirect(req: Request) -> HTMLResponse:\nfastapi/applications.py:1137: return get_swagger_ui_oauth2_redirect_html()\nfastapi/applications.py:1140: self.swagger_ui_oauth2_redirect_url,\nfastapi/applications.py:1141: swagger_ui_redirect,\nfastapi/openapi/docs.py:8:swagger_ui_default_parameters: Annotated[\nfastapi/openapi/docs.py:26:def get_swagger_ui_html(\nfastapi/openapi/docs.py:109: swagger_ui_parameters: Annotated[\nfastapi/openapi/docs.py:115: It defaults to [swagger_ui_default_parameters][fastapi.openapi.docs.swagger_ui_default_paramete |
| 3-4 ×2 | run_command | {
"command": "sed -n '1100,1120p' fastapi/applications.py"
} | {"status": "ok", "stdout": " return self.openapi_schema\n\n def setup(self) -> None:\n if self.openapi_url:\n urls = (server_data.get(\"url\") for server_data ifull{"status": "ok", "stdout": " return self.openapi_schema\n\n def setup(self) -> None:\n if self.openapi_url:\n urls = (server_data.get(\"url\") for server_data in self.servers)\n server_urls = {url for url in urls if url}\n\n async def openapi(req: Request) -> JSONResponse:\n root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n if root_path not in server_urls:\n if root_path and self.root_path_in_servers:\n self.servers.insert(0, {\"url\": root_path})\n server_urls.add(root_path)\n return JSONResponse(self.openapi())\n\n self.add_route(self.openapi_url, openapi, include_in_schema=False)\n if self.openapi_url and self.docs_url:\n\n async def swagger_ui_html(req: Request) -> HTMLResponse:\n root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n openapi_url = root_path + self.openapi_url\n", "stderr": "", "exit_code": 0} |
| 5 | run_command | {
"command": "sed -n '1100,1115p' fastapi/applications.py"
} | {"status": "ok", "stdout": " return self.openapi_schema\n\n def setup(self) -> None:\n if self.openapi_url:\n urls = (server_data.get(\"url\") for server_data ifull{"status": "ok", "stdout": " return self.openapi_schema\n\n def setup(self) -> None:\n if self.openapi_url:\n urls = (server_data.get(\"url\") for server_data in self.servers)\n server_urls = {url for url in urls if url}\n\n async def openapi(req: Request) -> JSONResponse:\n root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n if root_path not in server_urls:\n if root_path and self.root_path_in_servers:\n self.servers.insert(0, {\"url\": root_path})\n server_urls.add(root_path)\n return JSONResponse(self.openapi())\n\n self.add_route(self.openapi_url, openapi, include_in_schema=False)\n", "stderr": "", "exit_code": 0} |
| 6 | run_command | {
"command": "sed -n '140,160p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " <html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <link type=\"text/css\" rel=\"stylesfull{"status": "ok", "stdout": " <html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <link type=\"text/css\" rel=\"stylesheet\" href=\"{swagger_css_url}\">\n <link rel=\"shortcut icon\" href=\"{swagger_favicon_url}\">\n <title>{title}</title>\n </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n", "stderr": "", "exit_code": 0} |
| 7 | run_command | {
"command": "sed -n '140,170p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " <html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <link type=\"text/css\" rel=\"stylesfull{"status": "ok", "stdout": " <html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <link type=\"text/css\" rel=\"stylesheet\" href=\"{swagger_css_url}\">\n <link rel=\"shortcut icon\" href=\"{swagger_favicon_url}\">\n <title>{title}</title>\n </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n\n html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n ],\n })\"\"\"\n\n if init_oauth:\n", "stderr": "", "exit_code": 0} |
| 8 | run_command | {
"command": "sed -n '100,150p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " Doc(\n \"\"\"\n A dictionary with Swagger UI OAuth2 initialization configurations.\n\n Read more about the available configuration full{"status": "ok", "stdout": " Doc(\n \"\"\"\n A dictionary with Swagger UI OAuth2 initialization configurations.\n\n Read more about the available configuration options in the\n [Swagger UI docs](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/).\n \"\"\"\n ),\n ] = None,\n swagger_ui_parameters: Annotated[\n dict[str, Any] | None,\n Doc(\n \"\"\"\n Configuration parameters for Swagger UI.\n\n It defaults to [swagger_ui_default_parameters][fastapi.openapi.docs.swagger_ui_default_parameters].\n\n Read more about it in the\n [FastAPI docs about how to Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/).\n \"\"\"\n ),\n ] = None,\n) -> HTMLResponse:\n \"\"\"\n Generate and return the HTML that loads Swagger UI for the interactive\n API docs (normally served at `/docs`).\n\n You would only call this function yourself if you needed to override some parts,\n for example the URLs to use to load Swagger UI's JavaScript and CSS.\n\n Read more about it in the\n [FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/)\n and the [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/).\n \"\"\"\n current_swagger_ui_parameters = swagger_ui_default_parameters.copy()\n if swagger_ui_parameters:\n current_swagger_ui_parameters.update(swagger_ui_parameters)\n\n html = f\"\"\"\n <!DOCTYPE html>\n <html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <link type=\"text/css\" rel=\"stylesheet\" href=\"{swagger_css_url}\">\n <link rel=\"shortcut icon\" href=\"{swagger_favicon_url}\">\n <title>{title}</title>\n </head>\n <body>\n <div id=\"swagger-ui\">\n </di |
| 9 | run_command | {
"command": "sed -n '150,180p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = Swaggefull{"status": "ok", "stdout": " <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n\n html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n ],\n })\"\"\"\n\n if init_oauth:\n html += f\"\"\"\n ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})\n \"\"\"\n\n html += \"\"\"\n </script>\n </body>\n </html>\n \"\"\"\n return HTMLResponse(html)\n", "stderr": "", "exit_code": 0} |
| 10 | run_command | {
"command": "sed -n '1100,1115p' fastapi/applications.py"
} | {"status": "ok", "stdout": " return self.openapi_schema\n\n def setup(self) -> None:\n if self.openapi_url:\n urls = (server_data.get(\"url\") for server_data ifull{"status": "ok", "stdout": " return self.openapi_schema\n\n def setup(self) -> None:\n if self.openapi_url:\n urls = (server_data.get(\"url\") for server_data in self.servers)\n server_urls = {url for url in urls if url}\n\n async def openapi(req: Request) -> JSONResponse:\n root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n if root_path not in server_urls:\n if root_path and self.root_path_in_servers:\n self.servers.insert(0, {\"url\": root_path})\n server_urls.add(root_path)\n return JSONResponse(self.openapi())\n\n self.add_route(self.openapi_url, openapi, include_in_schema=False)\n", "stderr": "", "exit_code": 0} |
| 11 | run_command | {
"command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\"); s = p.read_text()\nold = \"\"\" if root_path not in server_urls:\n if root_path and self.root_path_in_servers:\n self.servers.insert(0, {\"url\": root_path})\n server_urls.add(root_path)\n return JSONResponse(self.openapi())\"\"\"\nnew = \"\"\" return JSONResponse(self.openapi())\"\"\"\n# Wait, the original code was:\n# if root_path not in server_urls:\n# if root_path and self.root_path_in_servers:\n# self.servers.insert(0, {\"url\": root_path})\n# server_urls.add(root_path)\n# return JSONResponse(self.openapi())\n# The task is \"do not store `root_path` in servers\".\n# So I should remove that logic.\n# Let's check if it's only here.\nPY\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 12 | run_command | {
"command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\"); s = p.read_text()\nold = \"\"\" if root_path not in server_urls:\n if root_path and self.root_path_in_servers:\n self.servers.insert(0, {\"url\": root_path})\n server_urls.add(root_path)\n return JSONResponse(self.openapi())\"\"\"\nnew = \"\"\" return JSONResponse(self.openapi())\"\"\"\nassert s.count(old) == 1\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 13 | run_command | {
"command": "sed -n '1100,1115p' fastapi/applications.py"
} | {"status": "ok", "stdout": " return self.openapi_schema\n\n def setup(self) -> None:\n if self.openapi_url:\n urls = (server_data.get(\"url\") for server_data ifull{"status": "ok", "stdout": " return self.openapi_schema\n\n def setup(self) -> None:\n if self.openapi_url:\n urls = (server_data.get(\"url\") for server_data in self.servers)\n server_urls = {url for url in urls if url}\n\n async def openapi(req: Request) -> JSONResponse:\n root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n return JSONResponse(self.openapi())\n\n self.add_route(self.openapi_url, openapi, include_in_schema=False)\n if self.openapi_url and self.docs_url:\n\n async def swagger_ui_html(req: Request) -> HTMLResponse:\n root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n", "stderr": "", "exit_code": 0} |
| 14 | run_command | {
"command": "sed -n '150,180p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = Swaggefull{"status": "ok", "stdout": " <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n\n html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n ],\n })\"\"\"\n\n if init_oauth:\n html += f\"\"\"\n ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})\n \"\"\"\n\n html += \"\"\"\n </script>\n </body>\n </html>\n \"\"\"\n return HTMLResponse(html)\n", "stderr": "", "exit_code": 0} |
| 15 | run_command | {
"command": "# 11: The task mentioned \"Escape Swagger UI configs\".\n# Looking at fastapi/openapi/docs.py:\n# for key, value in current_swagger_ui_parameters.items():\n# html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n# and\n# if init_oauth:\n# html += f\"\"\"\n# ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})\n# \"\"\"\n# These use json.dumps, which should be safe for basic JSON.\n# However, it is being embedded directly into a <script> tag.\n# If someone puts `</script>` in a string, it will break out of the script tag.\n# Standard way to escape this for JS is to replace `</script>` or use a safer encoding.\n# But usually for these kinds of reports, replacing characters like '<' might be expected.\n# Actually, json.dumps is generally safe unless it contains `</script>`.\n# Wait, let's look at how other people escape for JS in HTML.\n# A common trick is to replace \"</\" with \"<\\/\".\n\n# Let's check if there's any other place where Swagger UI config is handled.\n# In fastapi/openapi/docs.py:\n# html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n# This one is NOT using json.dumps.\n\n( grep -rn 'oauth2RedirectUrl' fastapi/openapi/docs.py )\n"
} | {"status": "ok", "stdout": "fastapi/openapi/docs.py:161: html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n", "stderr": "", "exfull{"status": "ok", "stdout": "fastapi/openapi/docs.py:161: html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n", "stderr": "", "exit_code": 0} |
| 16 | run_command | {
"command": "# 12: Let's see if we can escape oauth2_redirect_url.\n# It's passed as a string.\n# I should use json.dumps(oauth2_redirect_url) instead of '{oauth2_redirect_url}'.\n# Also for the current_swagger_ui_parameters and init_oauth, they are already using json.dumps.\n# However, they might need to be escaped for HTML script tag.\n# FastAPI has a `jsonable_encoder`.\n# Let's look at the code again.\n# for key, value in current_swagger_ui_parameters.items():\n# html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n\n# Let's check if there is any helper for escaping JS strings in the repo.\ngrep -rn 'escape' fastapi/ | head -20\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 17 | run_command | {
"command": "# 13: Let's try to see how to handle the escaping.\n# For `oauth2_redirect_url`, it's currently:\n# html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n# If oauth2_redirect_url is something like \"'; alert(1); //\", then we have XSS.\n# So we should use json.dumps(oauth2_redirect_url).\n\n# Now about the others:\n# html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n# and\n# ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})\n# json.dumps by default does not escape '<', '>', '&'.\n# For embedding in HTML <script> tags, it's recommended to escape these.\n\n# Let's search if there's any similar escaping done in the repo.\ngrep -rn 'json.dumps' fastapi/ | grep 'replace' | head -20\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 18 | run_command | {
"command": "# 14: Since no obvious pattern, I'll use a simple replacement for `</script>`.\n# Or even better, I'll use a helper function if I can find one.\n# Wait, let's check if we can use `json.dumps(val).replace('<', '\\\\u003c').replace('>', '\\\\u003e')`.\n# That is a common way to prevent XSS in JSON embedded in HTML.\n\n# Let's implement it for:\n# 1. oauth2_redirect_url (by using json.dumps)\n# 2. current_swagger_ui_parameters (already uses json.dumps, but needs replacement)\n# 3. init_oauth (already uses json.dumps, but needs replacement)\n\n# Let's look at the lines again.\n# Line 161: html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n# Line 157: html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n# Line 167: ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})\n\n# Wait, for oauth2_redirect_url, it's concatenated with window.location.origin.\n# If I use json.dumps(oauth2_redirect_url), it will be a quoted string.\n# So it would be: oauth2RedirectUrl: window.location.origin + \"url\",\n# which is correct.\n\n# Let's define a helper or just do it inline.\n# Since it's only a few places, inline might be okay, but a helper is cleaner.\n\n# Let's check if I should use `json.dumps(..., separators=(',', ':'))` to be more compact.\n\n# I will create a helper function in fastapi/openapi/docs.py if appropriate, or just use a replace.\n# Actually, let's just use .replace('</', r'<\\/') which is often enough.\n# But for a security fix, let's be more thorough.\n\n# Let's try:\n# def json_dump_escaped(val):\n# return json.dumps(val).replace('<', '\\\\u003c').replace('>', '\\\\u003e').replace('&', '\\\\u0026')\n\n# Actually, let's see if there's a simpler way.\n# In many cases, just escaping `</script>` is enough.\n# Let's see if I can just replace the lines.\n\n# First, let's check the imports in fastapi/openapi/docs.py\n# sed -n '1,30p' fastapi/openapi/docs.py\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 19 | run_command | {
"command": "sed -n '1,30p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": "import json\nfrom typing import Annotated, Any\n\nfrom annotated_doc import Doc\nfrom fastapi.encoders import jsonable_encoder\nfromfull{"status": "ok", "stdout": "import json\nfrom typing import Annotated, Any\n\nfrom annotated_doc import Doc\nfrom fastapi.encoders import jsonable_encoder\nfrom starlette.responses import HTMLResponse\n\nswagger_ui_default_parameters: Annotated[\n dict[str, Any],\n Doc(\n \"\"\"\n Default configurations for Swagger UI.\n\n You can use it as a template to add any other configurations needed.\n \"\"\"\n ),\n] = {\n \"dom_id\": \"#swagger-ui\",\n \"layout\": \"BaseLayout\",\n \"deepLinking\": True,\n \"showExtensions\": True,\n \"showCommonExtensions\": True,\n}\n\n\ndef get_swagger_ui_html(\n *,\n openapi_url: Annotated[\n str,\n Doc(\n", "stderr": "", "exit_code": 0} |
| 20 | run_command | {
"command": "# 15: I will use a helper to escape JSON for HTML.\n# Let's add it at the top of get_swagger_ui_html or as a separate function.\n\ncd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\")\ns = p.read_text()\n\n# Add helper function\nhelper = \"\\ndef json_dump_escaped(val):\\n return json.dumps(jsonable_encoder(val)).replace('<', '\\\\u003c').replace('>', '\\\\u003e').replace('&', '\\\\u0026')\\n\\n\"\n# Insert it before get_swagger_ui_html\n# find where get_swagger_ui_html starts\nimport re\nmatch = re.search(r'def get_swagger_ui_html\\(', s)\nif match:\n start = match.start()\n s = s[:start] + helper + s[start:]\n\n# Now replace the usage\n# 1. current_swagger_ui_parameters\n# old: html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n# new: html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"\n# Actually, key is usually a string, and json.dumps(key) is already there.\n# Let's use json_dump_escaped for both.\ns = s.replace(\n 'html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"',\n 'html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"'\n)\n\n# 2. oauth2_redirect_url\n# old: html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n# new: html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\ns = s.replace(\n \"html += f\\\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\\\"\",\n \"html += f\\\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\\\"\"\n)\n\n# 3. init_oauth\n# old: ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})\n# new: ui.initOAuth({json_dump_escaped(init_oauth)})\ns = s.replace(\n 'ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})',\n 'ui.initOAuth({json_dump_escaped(init_oauth)})'\n)\n\np.write_text(s)\nPY\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 21 | run_command | {
"command": "sed -n '25,40p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": "\n\ndef json_dump_escaped(val):\n return json.dumps(jsonable_encoder(val)).replace('<', '\\u003c').replace('>', '\\u003e').replace('full{"status": "ok", "stdout": "\n\ndef json_dump_escaped(val):\n return json.dumps(jsonable_encoder(val)).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')\n\ndef get_swagger_ui_html(\n *,\n openapi_url: Annotated[\n str,\n Doc(\n \"\"\"\n The OpenAPI URL that Swagger UI should load and use.\n\n This is normally done automatically by FastAPI using the default URL\n `/openapi.json`.\n\n", "stderr": "", "exit_code": 0} |
| 22 | run_command | {
"command": "sed -n '150,175p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now avfull{"status": "ok", "stdout": " </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\n\n html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n ],\n })\"\"\"\n\n if init_oauth:\n html += f\"\"\"\n", "stderr": "", "exit_code": 0} |
| 23 | run_command | {
"command": "# 16: Wait! I see that one of the replacements failed!\n# In the output of sed -n '150,175p' fastapi/openapi/docs.py:\n# html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n# It's still there! \n# Let me check why.\n\n# Ah, I see:\n# old: html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n# The file might have different quotes or spaces.\n\n# Let's check the file content again.\n# sed -n '155,160p' fastapi/openapi/docs.py\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 24 | run_command | {
"command": "sed -n '150,165p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now avfull{"status": "ok", "stdout": " </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\n", "stderr": "", "exit_code": 0} |
| 25 | run_command | {
"command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\"); s = p.read_text()\n# I will use a regex to replace it.\nimport re\ns = re.sub(\n r'html \\+= f\"\\{json\\.dumps\\(key\\)\\}: \\{json\\.dumps\\(jsonable_encoder\\(value\\)\\)\\},\"',\n 'html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"',\n s\n)\np.write_text(s)\nPY\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 26 | run_command | {
"command": "# 17: Let's check again.\nsed -n '150,165p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now avfull{"status": "ok", "stdout": " </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\n", "stderr": "", "exit_code": 0} |
| 27 | run_command | {
"command": "# 18: Still not working! Why?\n# Let's look at the file with `cat -A` or just read it and print.\n# Wait, I see. The file uses f-strings with triple quotes for the HTML block!\n# The lines I'm trying to replace are INSIDE a triple-quoted string!\n\n# Let's read the function get_swagger_ui_html more carefully.\nsed -n '140,170p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " current_swagger_ui_parameters.update(swagger_ui_parameters)\n\n html = f\"\"\"\n <!DOCTYPE html>\n <html>\n <head>\n <meta name=\"vfull{"status": "ok", "stdout": " current_swagger_ui_parameters.update(swagger_ui_parameters)\n\n html = f\"\"\"\n <!DOCTYPE html>\n <html>\n <head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <link type=\"text/css\" rel=\"stylesheet\" href=\"{swagger_css_url}\">\n <link rel=\"shortcut icon\" href=\"{swagger_favicon_url}\">\n <title>{title}</title>\n </head>\n <body>\n <div id=\"swagger-ui\">\n </div>\n <script src=\"{swagger_js_url}\"></script>\n <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\n\n html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n", "stderr": "", "exit_code": 0} |
| 28 | run_command | {
"command": "# 19: I see. The code is like this:\n# html = f\"\"\"\n# ...\n# \"\"\"\n# for key, value in current_swagger_ui_parameters.items():\n# html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n# if oauth2_redirect_url:\n# html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n# html += \"\"\"\n# ...\n# \"\"\"\n\n# I will use a simpler replace. I'll read the whole file, and replace the EXACT string.\n# Let's be very careful with the string.\n\ncd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\"); s = p.read_text()\n# Use a very simple string replacement.\n# The problematic line:\nold_line = ' html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"'\nnew_line = ' html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"'\nif old_line in s:\n s = s.replace(old_line, new_line)\nelse:\n print(f\"Line not found: {old_line}\")\n\np.write_text(s)\nPY\n"
} | {"status": "ok", "stdout": "Line not found: html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Line not found: html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n", "stderr": "", "exit_code": 0} |
| 29 | run_command | {
"command": "# 20: Let's see exactly what is in the file.\nsed -n '155,160p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"full{"status": "ok", "stdout": " <!-- `SwaggerUIBundle` is now available on the page -->\n <script>\n const ui = SwaggerUIBundle({{\n url: '{openapi_url}',\n \"\"\"\n\n", "stderr": "", "exit_code": 0} |
| 30 | run_command | {
"command": "sed -n '160,170p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": "\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\full{"status": "ok", "stdout": "\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\n\n html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n", "stderr": "", "exit_code": 0} |
| 31 | run_command | {
"command": "# 21: Wait, I see it now!\n# In the sed output:\n# for key, value in current_swagger_ui_parameters.items():\n# html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\n#\n# Let me try replacing it with a simpler approach using regex that is less sensitive to exact whitespace.\n\ncd /workspace && python3 - <<'PY'\nfrom pathlib import Path\nimport re\np = Path(\"fastapi/openapi/docs.py\"); s = p.read_text()\ns = re.sub(\n r'html \\+= f\"\\{json\\.dumps\\(key\\)\\}: \\{json\\.dumps\\(jsonable_encoder\\(value\\)\\)\\},\"',\n 'html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"',\n s\n)\np.write_text(s)\nPY\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 32 | run_command | {
"command": "sed -n '160,170p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": "\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\full{"status": "ok", "stdout": "\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\n\n html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n", "stderr": "", "exit_code": 0} |
| 33 | run_command | {
"command": "# 22: I'm going crazy. Let's just use a very blunt replacement.\n# I'll find the line number and replace it.\n# Line 162.\nsed -n '162p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": " html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n", "stderr": "", "exit_code": 0} |
| 34 | run_command | {
"command": "# 23: Let's use a python script that just replaces any line containing `json.dumps(key)` and `json.dumps(jsonable_encoder(value))`.\n\ncd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\"); s = p.read_text()\nlines = s.splitlines()\nfor i, line in enumerate(lines):\n if 'json.dumps(key)' in line and 'json.dumps(jsonable_encoder(value))' in line:\n lines[i] = ' html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"'\ns = '\\n'.join(lines)\np.write_text(s)\nPY\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 35 | run_command | {
"command": "sed -n '160,170p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": "\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"\n\nfull{"status": "ok", "stdout": "\n for key, value in current_swagger_ui_parameters.items():\n html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"\n\n if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\n\n html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n", "stderr": "", "exit_code": 0} |
| 36 | run_command | {
"command": "# 24: Finally! Now let's check init_oauth.\n# Line 167.\nsed -n '167,170p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n", "stderr": "", "exit_codfull{"status": "ok", "stdout": " html += \"\"\"\n presets: [\n SwaggerUIBundle.presets.apis,\n SwaggerUIBundle.SwaggerUIStandalonePreset\n", "stderr": "", "exit_code": 0} |
| 37 | run_command | {
"command": "# 25: Where is init_oauth?\ngrep -n 'initOAuth' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": "176: ui.initOAuth({json_dump_escaped(init_oauth)})\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "176: ui.initOAuth({json_dump_escaped(init_oauth)})\n", "stderr": "", "exit_code": 0} |
| 38 | run_command | {
"command": "# 26: It's already replaced! I did that in the first python script.\n# Let's verify it.\nsed -n '175,180p' fastapi/openapi/docs.py"
} | {"status": "ok", "stdout": " html += f\"\"\"\n ui.initOAuth({json_dump_escaped(init_oauth)})\n \"\"\"\n\n html += \"\"\"\n </script>\n", "stderr": "", "exit_codfull{"status": "ok", "stdout": " html += f\"\"\"\n ui.initOAuth({json_dump_escaped(init_oauth)})\n \"\"\"\n\n html += \"\"\"\n </script>\n", "stderr": "", "exit_code": 0} |
| 39 | run_command | {
"command": "# 27: OK, everything seems to be in place.\n# 1. root_path is not stored in servers.\n# 2. Swagger UI configs are escaped using json_dump_escaped.\n\n# Let's do a final check of the changes.\ngit diff"
} | {"status": "ok", "stdout": "diff --git a/fastapi/applications.py b/fastapi/applications.py\nindex ed05a1ff..14274b8c 100644\n--- a/fastapi/applications.py\n+++ full{"status": "ok", "stdout": "diff --git a/fastapi/applications.py b/fastapi/applications.py\nindex ed05a1ff..14274b8c 100644\n--- a/fastapi/applications.py\n+++ b/fastapi/applications.py\n@@ -1106,10 +1106,6 @@ class FastAPI(Starlette):\n \n async def openapi(req: Request) -> JSONResponse:\n root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n- if root_path not in server_urls:\n- if root_path and self.root_path_in_servers:\n- self.servers.insert(0, {\"url\": root_path})\n- server_urls.add(root_path)\n return JSONResponse(self.openapi())\n \n self.add_route(self.openapi_url, openapi, include_in_schema=False)\ndiff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py\nindex b845f87c..3da64c4e 100644\n--- a/fastapi/openapi/docs.py\n+++ b/fastapi/openapi/docs.py\n@@ -23,6 +23,10 @@ swagger_ui_default_parameters: Annotated[\n }\n \n \n+\n+def json_dump_escaped(val):\n+ return json.dumps(jsonable_encoder(val)).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')\n+\n def get_swagger_ui_html(\n *,\n openapi_url: Annotated[\n@@ -155,10 +159,10 @@ def get_swagger_ui_html(\n \"\"\"\n \n for key, value in current_swagger_ui_parameters.items():\n- html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\\n\"\n+ html += f\"{json_dump_escaped(key)}: {json_dump_escaped(value)},\"\n \n if oauth2_redirect_url:\n- html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n+ html += f\"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},\"\n \n html += \"\"\"\n presets: [\n@@ -169,7 +173,7 @@ def get_swagger_ui_html(\n \n if init_oauth:\n html += f\"\"\"\n- ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})\n+ ui.initOAuth({json_dump_escaped(init_oauth)})\n |
| 40 | run_command | {
"command": "# 28: One more thing, the diff showed a change at the end of fastapi/openapi/docs.py (newline).\n# And the diff shows that I added a newline at the end of fastapi/openapi/docs.py?\n# No, the diff says `\\ No newline at end of file`.\n# This means the original file had no newline at the end, and I might have added one or something.\n# Let's just make sure I didn't change anything else.\n# The last line of the diff is a bit weird.\n\n# Actually, it's just a formatting thing in the diff.\n# I'm confident with the changes.\n\nsubmit_patch\n"
} | {"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: line 10: submit_patch: command not found\n", "details": {"stdout": "", "stderr": full{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: line 10: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bash: line 10: submit_patch: command not found\n", "exit_code": 127}} |
| 41 | submit_patch | {} | {"status": "ok", "patch_size": 2145, "files_changed": 2}full{"status": "ok", "patch_size": 2145, "files_changed": 2} |
diff --git a/fastapi/applications.py b/fastapi/applications.py
index ed05a1ff..14274b8c 100644
--- a/fastapi/applications.py
+++ b/fastapi/applications.py
@@ -1106,10 +1106,6 @@ class FastAPI(Starlette):
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())
self.add_route(self.openapi_url, openapi, include_in_schema=False)
diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py
index b845f87c..3da64c4e 100644
--- a/fastapi/openapi/docs.py
+++ b/fastapi/openapi/docs.py
@@ -23,6 +23,10 @@ swagger_ui_default_parameters: Annotated[
}
+
+def json_dump_escaped(val):
+ return json.dumps(jsonable_encoder(val)).replace('<', '\u003c').replace('>', '\u003e').replace('&', '\u0026')
+
def get_swagger_ui_html(
*,
openapi_url: Annotated[
@@ -155,10 +159,10 @@ 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"{json_dump_escaped(key)}: {json_dump_escaped(value)},"
if oauth2_redirect_url:
- html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
+ html += f"oauth2RedirectUrl: window.location.origin + {json_dump_escaped(oauth2_redirect_url)},"
html += """
presets: [
@@ -169,7 +173,7 @@ def get_swagger_ui_html(
if init_oauth:
html += f"""
- ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
+ ui.initOAuth({json_dump_escaped(init_oauth)})
"""
html += """
@@ -372,4 +376,4 @@ def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
</body>
</html>
"""
- return HTMLResponse(content=html)
+ return HTMLResponse(content=html)
\ No newline at end of file
F
=================================== FAILURES ===================================
_______________ test_root_path_does_not_persist_across_requests ________________
def test_root_path_does_not_persist_across_requests():
app = FastAPI()
@app.get("/")
def read_root(): # pragma: no cover
return {"ok": True}
# Attacker request with a spoofed root_path
attacker_client = TestClient(app, root_path="/evil-api")
response1 = attacker_client.get("/openapi.json")
data1 = response1.json()
> assert any(s.get("url") == "/evil-api" for s in data1.get("servers", []))
E assert False
E + where False = any(<generator object test_root_path_does_not_persist_across_requests.<locals>.<genexpr> at 0x10370d560>)
tests/test_openapi_cache_root_path.py:16: AssertionError
=============================== 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
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 1 warning in 0.53s