mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
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:
parent
58e25ccf47
commit
bafd5419fa
2 changed files with 68 additions and 2 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue