Browse Source

vulnerability in Swagger UI template generation

A Cross-Site Scripting (XSS) vulnerability was identified in the get_swagger_ui_html() function, which generates the Swagger UI documentation page (/docs)
pull/14567/head
jeferson cardoso 7 months ago
committed by GitHub
parent
commit
c61b570931
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 293
      fastapi/openapi/docs.py

293
fastapi/openapi/docs.py

@ -110,51 +110,51 @@ def get_swagger_ui_html(
[FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/) [FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/)
and the [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/). and the [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/).
""" """
current_swagger_ui_parameters = swagger_ui_default_parameters.copy() swagger_config = {
if swagger_ui_parameters: "url": openapi_url,
current_swagger_ui_parameters.update(swagger_ui_parameters) "dom_id": "#swagger-ui",
"layout": "BaseLayout",
html = f""" "deepLinking": True,
<!DOCTYPE html> "showExtensions": True,
<html> "showCommonExtensions": True,
<head> "presets": [
<link type="text/css" rel="stylesheet" href="{swagger_css_url}"> "SwaggerUIBundle.presets.apis",
<link rel="shortcut icon" href="{swagger_favicon_url}"> "SwaggerUIBundle.SwaggerUIStandalonePreset"
<title>{title}</title> ]
</head> }
<body>
<div id="swagger-ui">
</div>
<script src="{swagger_js_url}"></script>
<!-- `SwaggerUIBundle` is now available on the page -->
<script>
const ui = SwaggerUIBundle({{
url: '{openapi_url}',
"""
for key, value in current_swagger_ui_parameters.items(): if swagger_ui_parameters:
html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n" swagger_config.update(swagger_ui_parameters)
if oauth2_redirect_url: if oauth2_redirect_url:
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}'," swagger_config["oauth2RedirectUrl"] = f"window.location.origin + '{oauth2_redirect_url}'"
html += """ config_json = json.dumps(swagger_config, default=str)
presets: [
SwaggerUIBundle.presets.apis, html = f'''<!DOCTYPE html>
SwaggerUIBundle.SwaggerUIStandalonePreset <html>
], <head>
})""" <link type="text/css" rel="stylesheet" href="{swagger_css_url}">
<link rel="shortcut icon" href="{swagger_favicon_url}">
<title>{title}</title>
</head>
<body>
<div id="swagger-ui">
</div>
<script src="{swagger_js_url}"></script>
<script>
const ui = SwaggerUIBundle({config_json});'''
if init_oauth: if init_oauth:
html += f""" init_oauth_json = json.dumps(init_oauth, default=str)
ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))}) html += f'''
""" ui.initOAuth({init_oauth_json});'''
html += '''
</script>
</body>
</html>'''
html += """
</script>
</body>
</html>
"""
return HTMLResponse(html) return HTMLResponse(html)
@ -216,40 +216,40 @@ def get_redoc_html(
Read more about it in the Read more about it in the
[FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/). [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/).
""" """
html = f"""
<!DOCTYPE html> html = f'''<!DOCTYPE html>
<html> <html>
<head> <head>
<title>{title}</title> <title>{title}</title>
<!-- needed for adaptive design --> <!-- needed for adaptive design -->
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
""" '''
if with_google_fonts: if with_google_fonts:
html += """ html += '''<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> '''
"""
html += f""" html += f'''<link rel="shortcut icon" href="{redoc_favicon_url}">
<link rel="shortcut icon" href="{redoc_favicon_url}"> <!--
<!-- ReDoc doesn't change outer page styles
ReDoc doesn't change outer page styles -->
--> <style>
<style> body {{
body {{ margin: 0;
margin: 0; padding: 0;
padding: 0; }}
}} </style>
</style> </head>
</head> <body>
<body> <noscript>
<noscript> ReDoc requires Javascript to function. Please enable it to browse the documentation.
ReDoc requires Javascript to function. Please enable it to browse the documentation. </noscript>
</noscript> <redoc spec-url="{openapi_url}"></redoc>
<redoc spec-url="{openapi_url}"></redoc> <script src="{redoc_js_url}"> </script>
<script src="{redoc_js_url}"> </script> </body>
</body> </html>'''
</html>
"""
return HTMLResponse(html) return HTMLResponse(html)
@ -259,86 +259,85 @@ def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
You normally don't need to use or change this. You normally don't need to use or change this.
""" """
# copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html
html = """
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}
arr = qp.split("&"); html_content = """<!doctype html>
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';}); <html lang="en-US">
qp = qp ? JSON.parse('{' + arr.join() + '}', <head>
function (key, value) { <title>Swagger UI: OAuth2 Redirect</title>
return key === "" ? value : decodeURIComponent(value); </head>
} <body>
) : {}; <script>
'use strict';
isValid = qp.state === sentState; function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
if (( var sentState = oauth2.state;
oauth2.auth.schema.get("flow") === "accessCode" || var redirectUrl = oauth2.redirectUrl;
oauth2.auth.schema.get("flow") === "authorizationCode" || var isValid, qp, arr;
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) { if (/code|token|error/.test(window.location.hash)) {
if (!isValid) { qp = window.location.hash.substring(1).replace('?', '&');
oauth2.errCb({ } else {
authId: oauth2.auth.name, qp = location.search.substring(1);
source: "auth", }
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) { arr = qp.split("&");
delete oauth2.state; arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
oauth2.auth.code = qp.code; qp = qp ? JSON.parse('{' + arr.join() + '}',
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl}); function (key, value) {
} else { return key === "" ? value : decodeURIComponent(value);
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
} }
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else { } else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl}); let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
} }
window.close(); } else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
} }
window.close();
}
if (document.readyState !== 'loading') { if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run(); run();
} else { });
document.addEventListener('DOMContentLoaded', function () { }
run(); </script>
}); </body>
} </html>"""
</script>
</body> return HTMLResponse(content=html_content)
</html>
"""
return HTMLResponse(content=html)

Loading…
Cancel
Save