committed by
GitHub
4 changed files with 137 additions and 9 deletions
@ -0,0 +1,75 @@ |
|||
from fastapi import FastAPI |
|||
from fastapi.testclient import TestClient |
|||
|
|||
|
|||
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", [])) |
|||
|
|||
# Subsequent legitimate request with no root_path |
|||
clean_client = TestClient(app) |
|||
response2 = clean_client.get("/openapi.json") |
|||
data2 = response2.json() |
|||
servers = [s.get("url") for s in data2.get("servers", [])] |
|||
assert "/evil-api" not in servers |
|||
|
|||
|
|||
def test_multiple_different_root_paths_do_not_accumulate(): |
|||
app = FastAPI() |
|||
|
|||
@app.get("/") |
|||
def read_root(): # pragma: no cover |
|||
return {"ok": True} |
|||
|
|||
for prefix in ["/path-a", "/path-b", "/path-c"]: |
|||
c = TestClient(app, root_path=prefix) |
|||
c.get("/openapi.json") |
|||
|
|||
# A clean request should not have any of them |
|||
clean_client = TestClient(app) |
|||
response = clean_client.get("/openapi.json") |
|||
data = response.json() |
|||
servers = [s.get("url") for s in data.get("servers", [])] |
|||
for prefix in ["/path-a", "/path-b", "/path-c"]: |
|||
assert prefix not in servers, ( |
|||
f"root_path '{prefix}' leaked into clean request: {servers}" |
|||
) |
|||
|
|||
|
|||
def test_legitimate_root_path_still_appears(): |
|||
app = FastAPI() |
|||
|
|||
@app.get("/") |
|||
def read_root(): # pragma: no cover |
|||
return {"ok": True} |
|||
|
|||
client = TestClient(app, root_path="/api/v1") |
|||
response = client.get("/openapi.json") |
|||
data = response.json() |
|||
servers = [s.get("url") for s in data.get("servers", [])] |
|||
assert "/api/v1" in servers |
|||
|
|||
|
|||
def test_configured_servers_not_mutated(): |
|||
configured_servers = [{"url": "https://prod.example.com"}] |
|||
app = FastAPI(servers=configured_servers) |
|||
|
|||
@app.get("/") |
|||
def read_root(): # pragma: no cover |
|||
return {"ok": True} |
|||
|
|||
# Request with a rogue root_path |
|||
attacker_client = TestClient(app, root_path="/evil") |
|||
attacker_client.get("/openapi.json") |
|||
|
|||
# The original servers list must be untouched |
|||
assert configured_servers == [{"url": "https://prod.example.com"}] |
|||
@ -0,0 +1,37 @@ |
|||
from fastapi.openapi.docs import get_swagger_ui_html |
|||
|
|||
|
|||
def test_init_oauth_html_chars_are_escaped(): |
|||
xss_payload = "Evil</script><script>alert(1)</script>" |
|||
html = get_swagger_ui_html( |
|||
openapi_url="/openapi.json", |
|||
title="Test", |
|||
init_oauth={"appName": xss_payload}, |
|||
) |
|||
body = html.body.decode() |
|||
|
|||
assert "</script><script>" not in body |
|||
assert "\\u003c/script\\u003e\\u003cscript\\u003e" in body |
|||
|
|||
|
|||
def test_swagger_ui_parameters_html_chars_are_escaped(): |
|||
html = get_swagger_ui_html( |
|||
openapi_url="/openapi.json", |
|||
title="Test", |
|||
swagger_ui_parameters={"customKey": "<img src=x onerror=alert(1)>"}, |
|||
) |
|||
body = html.body.decode() |
|||
assert "<img src=x onerror=alert(1)>" not in body |
|||
assert "\\u003cimg" in body |
|||
|
|||
|
|||
def test_normal_init_oauth_still_works(): |
|||
html = get_swagger_ui_html( |
|||
openapi_url="/openapi.json", |
|||
title="Test", |
|||
init_oauth={"clientId": "my-client", "appName": "My App"}, |
|||
) |
|||
body = html.body.decode() |
|||
assert '"clientId": "my-client"' in body |
|||
assert '"appName": "My App"' in body |
|||
assert "ui.initOAuth" in body |
|||
Loading…
Reference in new issue