← pod_r2_v1f

fastapi_14986

failed WRONG_FIX wrong_fix · 32 tool calls · 282 s · fastapi/fastapi

Task input

♻️ 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 calls (33)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"root_path\" fastapi/openapi.py fastapi/openapi/utils.py | head -20"
}
{"status": "ok", "stdout": "", "stderr": "grep: fastapi/openapi.py: No such file or directory\n", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "grep: fastapi/openapi.py: No such file or directory\n", "exit_code": 0}
2run_command
{
 "command": "find fastapi -name \"*openapi*\""
}
{"status": "ok", "stdout": "fastapi/openapi\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "fastapi/openapi\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "grep -rn \"root_path\" fastapi/openapi | head -20"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
4run_command
{
 "command": "grep -rn \"root_path\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/applications.py:300: (`root_path`) if it's different from `/`.\nfastapi/applications.py:637: \"openapi_prefix\" has been dep
full
{"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\n", "stderr": "", "exit_code": 0}
5run_command
{
 "command": "grep -rn \"swagger_ui_parameters\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/applications.py:756: swagger_ui_parameters: Annotated[\nfastapi/applications.py:891: self.swagger_ui_parameters = swagger_ui
full
{"status": "ok", "stdout": "fastapi/applications.py:756:        swagger_ui_parameters: Annotated[\nfastapi/applications.py:891:        self.swagger_ui_parameters = swagger_ui_parameters\nfastapi/applications.py:1129:                    swagger_ui_parameters=self.swagger_ui_parameters,\nfastapi/openapi/docs.py:109:    swagger_ui_parameters: Annotated[\nfastapi/openapi/docs.py:134:    current_swagger_ui_parameters = swagger_ui_default_parameters.copy()\nfastapi/openapi/docs.py:135:    if swagger_ui_parameters:\nfastapi/openapi/docs.py:136:        current_swagger_ui_parameters.update(swagger_ui_parameters)\nfastapi/openapi/docs.py:157:    for key, value in current_swagger_ui_parameters.items():\n", "stderr": "", "exit_code": 0}
6run_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 i
full
{"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}
7run_command
{
 "command": "sed -n '130,160p' fastapi/openapi/docs.py"
}
{"status": "ok", "stdout": " Read more about it in the\n [FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/)\n an
full
{"status": "ok", "stdout": "    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    </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}
8run_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 i
full
{"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}
9run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\")\ns = 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)\"\"\"\nnew = \"\"\"                if root_path not in server_urls:\n                    pass\"\"\"\n# Wait, the problem says \"OpenAPI, do not store root_path in servers\".\n# The current code inserts it into self.servers.\n# If I just remove it, it won't be stored.\n# But wait, if I just remove the block, I should also remove the if root_path not in server_urls.\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
10run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\")\ns = 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)\"\"\"\nnew = \"\"\"                if root_path not in server_urls:\n                    pass\"\"\"\n# Actually, the request is \"OpenAPI, do not store root_path in servers\".\n# The code is:\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#\n# If I remove this, root_path will not be added to self.servers.\n# Let's just remove the whole block.\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\")\ns = 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)\"\"\"\nnew = \"\"\n# I need to be careful about indentation and the surrounding code.\n# Let's use a more precise replacement.\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
12run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\")\ns = 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# I will replace it with nothing, but I must ensure the indentation of the next line is correct.\n# The next line is \"                return JSONResponse(self.openapi())\"\n# Let's see the context again.\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
13run_command
{
 "command": "sed -n '1105,1112p' fastapi/applications.py"
}
{"status": "ok", "stdout": " server_urls = {url for url in urls if url}\n\n async def openapi(req: Request) -> JSONResponse:\n root_path = req.scope.get(\"root_
full
{"status": "ok", "stdout": "            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", "stderr": "", "exit_code": 0}
14run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\")\ns = 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)\"\"\"\nnew = \"\"\n# I will replace it with an empty string.\n# Wait, if I replace it with an empty string, the indentation of the next line \"                return JSONResponse(self.openapi())\" will be fine.\n# But I should probably remove the empty line if it's created.\n# Let's just replace the whole block including the if.\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}
15run_command
{
 "command": "sed -n '1105,1115p' fastapi/applications.py"
}
{"status": "ok", "stdout": " server_urls = {url for url in urls if url}\n\n async def openapi(req: Request) -> JSONResponse:\n root_path = req.scope.get(\"root_
full
{"status": "ok", "stdout": "            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\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", "stderr": "", "exit_code": 0}
16run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\")\ns = p.read_text()\n# Remove the extra newline created by the replacement\n# The current state is:\n#                root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n#\n#                return JSONResponse(self.openapi())\n#\n# I want:\n#                root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n#                return JSONResponse(self.openapi())\n#\n# But wait, root_path is now unused in that function.\n# If root_path is unused, I should remove it too.\n#\n# Let's check if root_path is used anywhere else in that function.\n# No, it was only used in the if block I removed.\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
17run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/applications.py\")\ns = p.read_text()\nold = \"\"\"            async def openapi(req: Request) -> JSONResponse:\n                root_path = req.scope.get(\"root_path\", \"\").rstrip(\"/\")\n\n                return JSONResponse(self.openapi())\"\"\"\nnew = \"\"\"            async def openapi(req: Request) -> JSONResponse:\n                return JSONResponse(self.openapi())\"\"\"\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}
18run_command
{
 "command": "sed -n '1105,1115p' fastapi/applications.py"
}
{"status": "ok", "stdout": " server_urls = {url for url in urls if url}\n\n async def openapi(req: Request) -> JSONResponse:\n return JSONResponse(self.openapi(
full
{"status": "ok", "stdout": "            server_urls = {url for url in urls if url}\n\n            async def openapi(req: Request) -> JSONResponse:\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}
19run_command
{
 "command": "sed -n '150,165p' 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 = Swagge
full
{"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", "stderr": "", "exit_code": 0}
20run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\")\ns = p.read_text()\nold = \"\"\"    for key, value in current_swagger_ui_parameters.items():\n        html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"»\n# Wait, the quote is wrong.\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": " File \"<stdin>\", line 4\n old = \"\"\" for key, value in current_swagger_ui_parameters.ite
full
{"status": "error", "error_type": "CommandError", "error_message": "  File \"<stdin>\", line 4\n    old = \"\"\"    for key, value in current_swagger_ui_parameters.items():\n          ^\nSyntaxError: unterminated triple-quoted string literal (detected at line 6)\n", "details": {"stdout": "", "stderr": "  File \"<stdin>\", line 4\n    old = \"\"\"    for key, value in current_swagger_ui_parameters.items():\n          ^\nSyntaxError: unterminated triple-quoted string literal (detected at line 6)\n", "exit_code": 1}}
21run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\")\ns = p.read_text()\nold = \"\"\"    for key, value in current_swagger_ui_parameters.items():\n        html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"\"\"\nnew = \"\"\"    for key, value in current_swagger_ui_parameters.items():\n        html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\"»\n# Still wrong. I should use a different way to escape the f-string.\nPY\n"
}
{"status": "error", "error_type": "CommandError", "error_message": " File \"<stdin>\", line 6\n new = \"\"\" for key, value in current_swagger_ui_parameters.ite
full
{"status": "error", "error_type": "CommandError", "error_message": "  File \"<stdin>\", line 6\n    new = \"\"\"    for key, value in current_swagger_ui_parameters.items():\n          ^\nSyntaxError: unterminated triple-quoted string literal (detected at line 8)\n", "details": {"stdout": "", "stderr": "  File \"<stdin>\", line 6\n    new = \"\"\"    for key, value in current_swagger_ui_parameters.items():\n          ^\nSyntaxError: unterminated triple-quoted string literal (detected at line 8)\n", "exit_code": 1}}
22run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\")\ns = p.read_text()\n# I want to change:\n#     for key, value in current_swagger_ui_parameters.items():\n#         html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n# to something that escapes the data.\n# Actually, json.dumps already escapes the data for JSON.\n# But this is being inserted into a JavaScript object literal inside an HTML script tag.\n# The issue is about \"escaping data\".\n# If json.dumps is used, it should be safe from most XSS if it's just a JS object.\n# However, if the value contains `</script>`, it could close the script tag.\n#\n# Wait, the problem says \"Escape Swagger UI configs\".\n# Let's see how it's currently done:\n# html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n#\n# If I use json.dumps, it produces a JSON string.\n# If I want to be extra safe, I should make sure that it doesn't contain `</script>`.\n#\n# But wait, maybe the \"escaping\" they mean is just using json.dumps?\n# It already uses json.dumps.\n#\n# Let's re-read: \"Escape Swagger UI configs: I wouldn't consider this really important... but probably also doesn't hurt much to have it there.\"\n#\n# If I look at the current code:\n# for key, value in current_swagger_ui_parameters.items():\n#     html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n#\n# This is already using json.dumps.\n# Maybe they want to use a more robust way to embed JSON in HTML?\n#\n# Actually, if I look at the code again:\n# html = f\"\"\"\n# ...\n# <script>\n# const ui = SwaggerUIBundle({{\n#     url: '{openapi_url}',\n# \"\"\"\n#\n# Then the loop:\n# for key, value in current_swagger_ui_parameters.items():\n#     html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n#\n# The `openapi_url` is NOT json.dumps-ed. It's just `{openapi_url}`.\n# That could be a problem.\n#\n# Let's check if openapi_url is escaped.\n# openapi_url = root_path + self.openapi_url\n#\n# If I change `{openapi_url}` to `{json.dumps(openapi_url)}`, it would be safer.\n# But wait, the current code is:\n# url: '{openapi_url}',\n#\n# If I change it to:\n# url: {json.dumps(openapi_url)},\n# it would be safer.\n#\n# Let's check the loop again.\n# html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n#\n# This part seems okay because it uses json.dumps.\n#\n# Wait, I see something.\n# The `openapi_url` is inside single quotes: `url: '{openapi_url}',`\n# If `openapi_url` contains a single quote, it will break the JS.\n#\n# If I change it to `url: {json.dumps(openapi_url)},`, it will be a proper JS string.\n#\n# Let's see where `openapi_url` is defined in `fastapi/openapi/docs.py`.\n# It's passed as an argument to `get_swagger_ui_html`.\n#\n# Let's look at `fastapi/openapi/docs.py` again.\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
23run_command
{
 "command": "sed -n '140,155p' 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=\"styles
full
{"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", "stderr": "", "exit_code": 0}
24run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\")\ns = p.read_text()\nold = \"        url: '{openapi_url}',\"\nnew = \"        url: {json.dumps(openapi_url)},\"\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}
25run_command
{
 "command": "sed -n '150,155p' 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 = Swagge
full
{"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: {json.dumps(openapi_url)},\n    \"\"\"\n", "stderr": "", "exit_code": 0}
26run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\")\ns = p.read_text()\n# Wait, I just realized that the f-string in `get_swagger_ui_html` is:\n# html = f\"\"\"\n# ...\n# <script>\n# const ui = SwaggerUIBundle({{\n#     url: {json.dumps(openapi_url)},\n# \"\"\"\n#\n# But `json` is not imported in `get_swagger_ui_html`'s scope if it's not imported in the module.\n# Let's check imports in `fastapi/openapi/docs.py`.\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
27run_command
{
 "command": "head -n 30 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\nfrom
full
{"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}
28run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\")\ns = p.read_text()\n# The f-string is:\n# html = f\"\"\"\n# ...\n# <script>\n# const ui = SwaggerUIBundle({{\n#     url: {json.dumps(openapi_url)},\n# \"\"\"\n#\n# But wait, this is an f-string. {json.dumps(openapi_url)} will be evaluated at runtime.\n# This is correct.\n#\n# However, I should check if there are other things that need escaping.\n# The loop:\n# for key, value in current_swagger_ui_parameters.items():\n#     html += f\"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n#\n# This is also using json.dumps.\n#\n# What about `oauth2_redirect_url`?\n# if oauth2_redirect_url:\n#     html += f\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\n#\n# This is also not using json.dumps.\n# Let's change it to:\n# html += f\"oauth2RedirectUrl: window.location.origin + {json.dumps(oauth2_redirect_url)},\"\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
29run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/openapi/docs.py\")\ns = p.read_text()\nold = \"        html += f\\\"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',\"\nnew = \"        html += f\\\"oauth2RedirectUrl: window.location.origin + {json.dumps(oauth2_redirect_url)},\"\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}
30run_command
{
 "command": "sed -n '160,170p' fastapi/openapi/docs.py"
}
{"status": "ok", "stdout": " if oauth2_redirect_url:\n html += f\"oauth2RedirectUrl: window.location.origin + {json.dumps(oauth2_redirect_url)},\"\n\n html += \
full
{"status": "ok", "stdout": "    if oauth2_redirect_url:\n        html += f\"oauth2RedirectUrl: window.location.origin + {json.dumps(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}
31run_command
{
 "command": "git diff"
}
{"status": "ok", "stdout": "diff --git a/fastapi/applications.py b/fastapi/applications.py\nindex ed05a1ff..82dbfb69 100644\n--- a/fastapi/applications.py\n+++
full
{"status": "ok", "stdout": "diff --git a/fastapi/applications.py b/fastapi/applications.py\nindex ed05a1ff..82dbfb69 100644\n--- a/fastapi/applications.py\n+++ b/fastapi/applications.py\n@@ -1105,11 +1105,6 @@ class FastAPI(Starlette):\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)\ndiff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py\nindex b845f87c..92acff08 100644\n--- a/fastapi/openapi/docs.py\n+++ b/fastapi/openapi/docs.py\n@@ -151,14 +151,14 @@ def get_swagger_ui_html(\n     <!-- `SwaggerUIBundle` is now available on the page -->\n     <script>\n     const ui = SwaggerUIBundle({{\n-        url: '{openapi_url}',\n+        url: {json.dumps(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+        html += f\"oauth2RedirectUrl: window.location.origin + {json.dumps(oauth2_redirect_url)},\"\n \n     html += \"\"\"\n     presets: [\n", "stderr": "", "exit_code": 0}
32run_command
{
 "command": "submit_patch"
}
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bas
full
{"status": "error", "error_type": "CommandError", "error_message": "/bin/bash: submit_patch: command not found\n", "details": {"stdout": "", "stderr": "/bin/bash: submit_patch: command not found\n", "exit_code": 127}}
33submit_patch
{}
{"status": "ok", "patch_size": 1557, "files_changed": 2}
full
{"status": "ok", "patch_size": 1557, "files_changed": 2}

Patch

diff --git a/fastapi/applications.py b/fastapi/applications.py
index ed05a1ff..82dbfb69 100644
--- a/fastapi/applications.py
+++ b/fastapi/applications.py
@@ -1105,11 +1105,6 @@ class FastAPI(Starlette):
             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())
 
             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..92acff08 100644
--- a/fastapi/openapi/docs.py
+++ b/fastapi/openapi/docs.py
@@ -151,14 +151,14 @@ def get_swagger_ui_html(
     <!-- `SwaggerUIBundle` is now available on the page -->
     <script>
     const ui = SwaggerUIBundle({{
-        url: '{openapi_url}',
+        url: {json.dumps(openapi_url)},
     """
 
     for key, value in current_swagger_ui_parameters.items():
         html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n"
 
     if oauth2_redirect_url:
-        html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
+        html += f"oauth2RedirectUrl: window.location.origin + {json.dumps(oauth2_redirect_url)},"
 
     html += """
     presets: [

Test output

show
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 0x105a99560>)

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.43s