DEV Community

Cover image for FastAPI escaped two values in its docs page and left three unescaped
Jonathan Santilli
Jonathan Santilli

Posted on

FastAPI escaped two values in its docs page and left three unescaped

The three items left are the ones that come from the request.

Affects FastAPI 0.140.0 → 0.141.1 (latest at time of writing)
Status Unfixed
Reported 2026-07-26
Declined 2026-08-11

Fourth of five write-ups on findings reported privately to FastAPI and closed without publication. This is the one where I think fault genuinely splits, and I'll say so before the reproduction rather than after.


What breaks

FastAPI generates the Swagger UI and ReDoc pages itself. Building them, it reads root_path from the request scope and concatenates it into the URLs those pages use:

root_path = req.scope.get("root_path", "").rstrip("/")     # applications.py L1124
openapi_url = root_path + self.openapi_url                 # L1125
oauth2_redirect_url = root_path + oauth2_redirect_url      # L1128
Enter fullscreen mode Exit fullscreen mode

Those values then land, unescaped, in two different parsing contexts — a single-quoted JavaScript string, a second JavaScript string, and a double-quoted HTML attribute:

url: '{openapi_url}',                                              # L168
oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',  # L175
<redoc spec-url="{openapi_url}"></redoc>                           # L293
Enter fullscreen mode Exit fullscreen mode

If request-controlled text can reach root_path, it escapes its context. Mounting a sub-application under a path parameter is one way to arrange that, using only public Starlette routing:

from fastapi import FastAPI

inner = FastAPI()
outer = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
outer.mount("/{tenant}", inner)
Enter fullscreen mode Exit fullscreen mode

Requesting the docs page with a crafted tenant value produces:

SWAGGER: url: '/x';globalThis.__X__=424242;'/openapi.json',
REDOC  : <redoc spec-url="/x"><img src=x onerror="..."></redoc>
CONTROL: url: '/acme/openapi.json',
Enter fullscreen mode Exit fullscreen mode

The control line is the point: an ordinary prefix passes through untouched. The failure is missing output encoding, not the mount.

Note also that the second sink is live by default — swagger_ui_oauth2_redirect_url has a default value, so that interpolation runs on a standard docs deployment.


The part that makes this interesting

FastAPI already fixed this class of bug in this exact function, five months ago, and stopped one line short.

PR #14986 added a helper to fastapi/openapi/docs.py:

def _html_safe_json(value: Any) -> str:
    """Serialize a value to JSON with HTML special characters escaped.

    This prevents injection when the JSON is embedded inside a <script> tag.
    """
Enter fullscreen mode Exit fullscreen mode

That commit applied the helper to swagger_ui_parameters and init_oauth, and added tests asserting that Evil</script><script>alert(1)</script> and <img src=x onerror=alert(1)> get neutralised.

In the diff, the unescaped oauth2RedirectUrl line sits directly between the two lines that were fixed.

The maintainer's stated reasoning is in the PR body, and it explains precisely why:

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.

That reasoning holds for the two values he escaped — swagger_ui_parameters and init_oauth are developer configuration. It does not hold for openapi_url and oauth2_redirect_url, which are assembled from the request. The two categories were treated as one.

Worth noting the same PR body also names the attacker path directly:

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.

And the tests added in that commit carry the comments "Attacker request with a spoofed root_path" and "Request with a rogue root_path." So root_path is already modelled as spoofable in FastAPI's own test suite.


Is it a vulnerability or a bug?

Why the framework is at fault

FastAPI generates this HTML. It's a built-in route, not application code, that reflects a request-derived value into a <script> string and an HTML attribute. Contextual output encoding belongs to whoever produces the output.

The fix standard was set in the same function. A helper exists, written by the maintainer, with a docstring naming this exact hazard. Three sibling interpolations don't use it.

Encoding costs nothing here. HTML-encoding the handful of dangerous characters in a URL path prefix cannot break a legitimate path. There's no compatibility argument against it. The maintainer's own words on the other escaping: "doesn't hurt to have it there."

Two of the three sinks are in a JavaScript string context, where HTML escaping alone is the wrong tool — they need script-safe JSON serialization, which is what the existing helper does. A single generic escape wouldn't be correct.

Why fault may sit with the application

This side is stronger here than in any other post in the series.

root_path is documented as trusted configuration. The behind-a-proxy docs describe it as set by the --root-path CLI option or the FastAPI(root_path=...) constructor. Both are deployment configuration. The documented forwarded headers are X-Forwarded-For, -Proto and -Host — not X-Forwarded-Prefix — and even those are ignored unless the server is explicitly told to trust the proxy.

The demonstrated vector is undocumented usage. FastAPI's sub-applications documentation only ever shows static mounts. A path-parameter mount is mechanically supported by Starlette but isn't a pattern FastAPI blesses.

The ecosystem already adjudicated this exact sink — against the application. CVE-2025-53528 was assigned to Cadwyn, a third-party library, for feeding a user-controlled value into the same Swagger UI helper. The advisory states the helper "does not encode or sanitize its arguments" and "is not intended to be used with user-controlled arguments." The CVE went to the application, not to FastAPI.

The maintainer has stated his position publicly, in the same PR body:

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.

Where I land

Shared, and I don't think that's a dodge.

The missing output encoding is a framework defect — small, cheap to fix, and inconsistent with what the same function already does three lines away. Whether it's exploitable depends on a deployment choice FastAPI doesn't make for you. On a default --root-path deployment it isn't reachable at all.

So I wouldn't call this "an XSS in FastAPI." I'd call it incomplete hardening: the values that carry request data are the ones that didn't get the treatment the others did, and the reasoning recorded in the PR shows why — they were all assumed to be developer-supplied.


What you can do today

Only relevant if request-controlled text can reach your root_path — which, on a standard deployment, it can't:

  • Check whether anything writes scope["root_path"] from a header. The common pattern is custom ASGI middleware reading X-Forwarded-Prefix, which is client-supplied unless your proxy strips it. If you have that, make sure the proxy strips or overwrites the header.
  • Avoid mounting applications under path parameters with docs enabled on the inner app.
  • Disable the generated docs on deployments where either of the above applies and you can't fix them, or host the docs on an origin that carries no application credentials.

The fix

Encode each value for the context it lands in:

  • serialize openapi_url and oauth2_redirect_url through the existing _html_safe_json helper before placing them in inline JavaScript, dropping the surrounding quotes;
  • HTML-escape the ReDoc spec-url value for a quoted attribute;
  • extend the existing escaping tests to cover quotes, angle brackets, ampersands, </script> and Unicode line separators for these three values.

A single general-purpose escape isn't sufficient, because a JavaScript string and an HTML attribute have different parsing rules — which is exactly why the helper exists.


Status

Date Event
2026-02-24 PR #14986 adds _html_safe_json, applies it to two of five interpolations, shipped under "Refactors"
2026-07-26 Reported privately as GHSA-mx5q-q8gw-3v6c, severity medium, CWE-79
2026-08-11 Closed without publication, submission.accepted: false
2026-08-12 Re-verified against 0.141.1. All three sinks still unescaped

The advisory is private, so that ID is citable but not a link a you can follow.


Verified by reading FastAPI's source at tag 0.141.1 and running the reproduction on 2026-08-12. Code links are pinned to that tag rather than master, so the line numbers stay valid.

Top comments (0)