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
This commit is contained in:
Jeremiah Lowin 2026-03-07 12:10:05 -05:00 committed by GitHub
commit bafd5419fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 68 additions and 2 deletions

View file

@ -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()

View file

@ -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"}