From bafd5419fabbd30415c3b782e778ba971f8019e7 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 7 Mar 2026 12:10:05 -0500 Subject: [PATCH] Redact sensitive headers in OpenAPI provider debug logging (#3436) * Redact sensitive headers in OpenAPI provider debug logging (#3427) * Use safe-header allowlist instead of sensitive-header denylist for redaction --- .../server/providers/openapi/components.py | 23 ++++++++- .../openapi/test_openapi_features.py | 47 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/server/providers/openapi/components.py b/src/fastmcp/server/providers/openapi/components.py index 1f52033fa..7d7722666 100644 --- a/src/fastmcp/server/providers/openapi/components.py +++ b/src/fastmcp/server/providers/openapi/components.py @@ -29,6 +29,25 @@ from fastmcp.utilities.openapi.director import RequestDirector if TYPE_CHECKING: from fastmcp.server import Context +_SAFE_HEADERS = frozenset( + { + "accept", + "accept-encoding", + "accept-language", + "cache-control", + "connection", + "content-length", + "content-type", + "host", + "user-agent", + } +) + + +def _redact_headers(headers: httpx.Headers) -> dict[str, str]: + return {k: v if k.lower() in _SAFE_HEADERS else "***" for k, v in headers.items()} + + __all__ = [ "OpenAPIResource", "OpenAPIResourceTemplate", @@ -183,7 +202,9 @@ class OpenAPITool(Tool): # Send the request and process the response. try: - logger.debug(f"run - sending request; headers: {request.headers}") + logger.debug( + f"run - sending request; headers: {_redact_headers(request.headers)}" + ) response = await self._client.send(request) response.raise_for_status() diff --git a/tests/server/providers/openapi/test_openapi_features.py b/tests/server/providers/openapi/test_openapi_features.py index ef4aeb002..ac78f1b10 100644 --- a/tests/server/providers/openapi/test_openapi_features.py +++ b/tests/server/providers/openapi/test_openapi_features.py @@ -9,7 +9,10 @@ from httpx import Response from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.server.providers.openapi import OpenAPIProvider -from fastmcp.server.providers.openapi.components import _extract_mime_type_from_route +from fastmcp.server.providers.openapi.components import ( + _extract_mime_type_from_route, + _redact_headers, +) from fastmcp.server.providers.openapi.routing import MCPType, RouteMap from fastmcp.utilities.openapi.models import HTTPRoute, ResponseInfo @@ -982,3 +985,45 @@ class TestValidateOutput: assert get_user.outputSchema.get("additionalProperties") is True # Should NOT have specific properties from the original schema assert "properties" not in get_user.outputSchema + + +class TestRedactHeaders: + """Test that non-safe headers are redacted in debug logging.""" + + def test_known_sensitive_headers_are_redacted(self): + headers = httpx.Headers( + { + "Authorization": "Bearer secret-token", + "X-API-Key": "my-api-key", + "Cookie": "session=abc123", + "Proxy-Authorization": "Basic creds", + "Content-Type": "application/json", + "Accept": "text/html", + } + ) + redacted = _redact_headers(headers) + assert redacted["authorization"] == "***" + assert redacted["x-api-key"] == "***" + assert redacted["cookie"] == "***" + assert redacted["proxy-authorization"] == "***" + assert redacted["content-type"] == "application/json" + assert redacted["accept"] == "text/html" + + def test_arbitrary_auth_headers_are_redacted(self): + """Arbitrary header names (e.g. OpenAPI apiKey-in-header) are redacted.""" + headers = httpx.Headers( + { + "X-Custom-Token": "secret", + "X-My-Service-Key": "also-secret", + "Content-Type": "application/json", + } + ) + redacted = _redact_headers(headers) + assert redacted["x-custom-token"] == "***" + assert redacted["x-my-service-key"] == "***" + assert redacted["content-type"] == "application/json" + + def test_safe_only_headers(self): + headers = httpx.Headers({"Content-Type": "application/json"}) + redacted = _redact_headers(headers) + assert redacted == {"content-type": "application/json"}