Browse Source

fix: escape user-controlled values in Swagger UI and ReDoc HTML

The /docs and /redoc endpoints embed openapi_url, oauth2_redirect_url,
title, and CDN URLs directly into HTML and JavaScript using f-strings
without escaping. When root_path comes from a reverse proxy header
(like X-Forwarded-Prefix), an attacker can inject arbitrary JavaScript
into the generated page.

This applies html.escape() to values in HTML attribute and text contexts,
and uses the existing _html_safe_json() (which produces properly quoted
JSON strings) for values embedded in JavaScript contexts. This is the
same approach already used for swagger_ui_parameters.
pull/15423/head
Ismail Pelaseyed 3 months ago
parent
commit
b679bd3db2
  1. 21
      fastapi/openapi/docs.py
  2. 2
      tests/test_application.py
  3. 2
      tests/test_custom_swagger_ui_redirect.py

21
fastapi/openapi/docs.py

@ -1,4 +1,5 @@
import json
from html import escape as html_escape
from typing import Annotated, Any
from annotated_doc import Doc
@ -154,25 +155,25 @@ def get_swagger_ui_html(
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
<link rel="shortcut icon" href="{swagger_favicon_url}">
<title>{title}</title>
<link type="text/css" rel="stylesheet" href="{html_escape(swagger_css_url, quote=True)}">
<link rel="shortcut icon" href="{html_escape(swagger_favicon_url, quote=True)}">
<title>{html_escape(title)}</title>
</head>
<body>
<div id="swagger-ui">
</div>
<script src="{swagger_js_url}"></script>
<script src="{html_escape(swagger_js_url, quote=True)}"></script>
<!-- `SwaggerUIBundle` is now available on the page -->
<script>
const ui = SwaggerUIBundle({{
url: '{openapi_url}',
url: {_html_safe_json(openapi_url)},
"""
for key, value in current_swagger_ui_parameters.items():
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}',"
html += f"oauth2RedirectUrl: window.location.origin + {_html_safe_json(oauth2_redirect_url)},"
html += """
presets: [
@ -265,7 +266,7 @@ def get_redoc_html(
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<title>{html_escape(title)}</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -275,7 +276,7 @@ def get_redoc_html(
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
"""
html += f"""
<link rel="shortcut icon" href="{redoc_favicon_url}">
<link rel="shortcut icon" href="{html_escape(redoc_favicon_url, quote=True)}">
<!--
ReDoc doesn't change outer page styles
-->
@ -290,8 +291,8 @@ def get_redoc_html(
<noscript>
ReDoc requires Javascript to function. Please enable it to browse the documentation.
</noscript>
<redoc spec-url="{openapi_url}"></redoc>
<script src="{redoc_js_url}"> </script>
<redoc spec-url="{html_escape(openapi_url, quote=True)}"></redoc>
<script src="{html_escape(redoc_js_url, quote=True)}"> </script>
</body>
</html>
"""

2
tests/test_application.py

@ -27,7 +27,7 @@ def test_swagger_ui():
assert response.headers["content-type"] == "text/html; charset=utf-8"
assert "swagger-ui-dist" in response.text
assert (
"oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect'"
'oauth2RedirectUrl: window.location.origin + "/docs/oauth2-redirect"'
in response.text
)

2
tests/test_custom_swagger_ui_redirect.py

@ -21,7 +21,7 @@ def test_swagger_ui():
assert "swagger-ui-dist" in response.text
print(client.base_url)
assert (
f"oauth2RedirectUrl: window.location.origin + '{swagger_ui_oauth2_redirect_url}'"
f'oauth2RedirectUrl: window.location.origin + "{swagger_ui_oauth2_redirect_url}"'
in response.text
)

Loading…
Cancel
Save