From ccc139c1fd4e18fbb8f6edc14ddd896613b13078 Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 <336610228+slopggobblercobbler81@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:28:32 +0200 Subject: [PATCH 1/2] docs: clarify uvicorn server header limitation in README (fixes #7) --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 742a45b..793ad4f 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,21 @@ If this project helps you secure your FastAPI applications, consider buying a co ## 📄 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + + +## ℹ️ Important Note on Uvicorn Server Header + +When running behind **Uvicorn**, pure ASGI middleware runs *after* Uvicorn appends its server-level default headers (such as Server: uvicorn). Therefore, strip_server_headers=True will filter out headers from the app layer, but Uvicorn's own wire-level Server header will still be sent unless disabled directly in Uvicorn: + +`python +# Run uvicorn programmatically with server_header=False +import uvicorn + +uvicorn.run(app, host='127.0.0.1', port=8000, server_header=False) +` + +Or via the CLI command: +`ash +# Or pass --no-server-header flag in CLI +uvicorn app:app --no-server-header +` From 6af15babf303f86e4453074076b2365d8df37794 Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 <336610228+slopggobblercobbler81@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:13:06 +0200 Subject: [PATCH 2/2] docs: fix markdown formatting and add test coverage for strip_server_headers --- README.md | 45 +++++++++++++++++++++++++--------------- tests/test_middleware.py | 34 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 793ad4f..e489498 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,34 @@ app.add_middleware(SecurityHeadersMiddleware, override=True) --- +### Stripping Server Headers + +To strip `Server` and `X-Powered-By` headers emitted by application routes (to mitigate server fingerprinting), set `strip_server_headers=True`: + +```python +app.add_middleware(SecurityHeadersMiddleware, strip_server_headers=True) +``` + +#### ⚠️ Important Note on Uvicorn Server Header + +When running behind **Uvicorn**, pure ASGI middleware runs *after* Uvicorn appends its server-level default headers (such as `Server: uvicorn`). Therefore, `strip_server_headers=True` will filter out headers from the application layer, but Uvicorn's own wire-level `Server` header will still be sent unless disabled directly in Uvicorn: + +**Programmatically:** +```python +# Run uvicorn programmatically with server_header=False +import uvicorn + +uvicorn.run(app, host="127.0.0.1", port=8000, server_header=False) +``` + +**Or via the CLI flag:** +```bash +# Pass --no-server-header flag in CLI +uvicorn app:app --no-server-header +``` + +--- + ## 🧪 Testing Run the test suite locally with `pytest`: @@ -186,20 +214,3 @@ If this project helps you secure your FastAPI applications, consider buying a co This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## ℹ️ Important Note on Uvicorn Server Header - -When running behind **Uvicorn**, pure ASGI middleware runs *after* Uvicorn appends its server-level default headers (such as Server: uvicorn). Therefore, strip_server_headers=True will filter out headers from the app layer, but Uvicorn's own wire-level Server header will still be sent unless disabled directly in Uvicorn: - -`python -# Run uvicorn programmatically with server_header=False -import uvicorn - -uvicorn.run(app, host='127.0.0.1', port=8000, server_header=False) -` - -Or via the CLI command: -`ash -# Or pass --no-server-header flag in CLI -uvicorn app:app --no-server-header -` diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 64edd85..24f08be 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -131,3 +131,37 @@ async def ws_endpoint(websocket: WebSocket): ws.send_text("hello-asgi") data = ws.receive_text() assert data == "echo:hello-asgi" + + +def test_strip_server_headers_enabled(create_app): + """When strip_server_headers=True, Server and X-Powered-By should be stripped.""" + app = create_app(strip_server_headers=True) + client = TestClient(app) + + response = client.get("/server-header") + assert response.status_code == 200 + assert "server" not in response.headers + assert "x-powered-by" not in response.headers + + +def test_strip_server_headers_disabled_by_default(create_app): + """When strip_server_headers is False (default), Server and X-Powered-By should be preserved.""" + app = create_app() + client = TestClient(app) + + response = client.get("/server-header") + assert response.status_code == 200 + assert response.headers.get("server") == "uvicorn" + assert response.headers.get("x-powered-by") == "FastAPI" + + +def test_strip_server_headers_via_config(create_app): + """When strip_server_headers=True is set via SecurityHeadersConfig.""" + config = SecurityHeadersConfig(strip_server_headers=True) + app = create_app(config=config) + client = TestClient(app) + + response = client.get("/server-header") + assert response.status_code == 200 + assert "server" not in response.headers + assert "x-powered-by" not in response.headers