diff --git a/README.md b/README.md index 742a45b..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`: @@ -185,3 +213,4 @@ 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. + 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