From 37e4637dc4d2e0ba925aeb5f6586ad0874b183d1 Mon Sep 17 00:00:00 2001 From: oopswoo3 <102633566+oopswoo3@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:52:05 +0800 Subject: [PATCH] fix(mcp): redact sensitive headers in MCP request logs Mask apikey, Authorization, and related credentials before McpLogger writes request headers to console and MCP_FILE, without changing auth behavior. --- .../UnifiedMultiTenancyFilter.java | 14 +---- .../common/util/SensitiveHeaderLogMask.java | 63 +++++++++++++++++++ .../util/SensitiveHeaderLogMaskTest.java | 30 +++++++++ 3 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 src/main/java/ai/nubase/common/util/SensitiveHeaderLogMask.java create mode 100644 src/test/java/ai/nubase/common/util/SensitiveHeaderLogMaskTest.java diff --git a/src/main/java/ai/nubase/common/multitenancy/UnifiedMultiTenancyFilter.java b/src/main/java/ai/nubase/common/multitenancy/UnifiedMultiTenancyFilter.java index d604556..a58514c 100644 --- a/src/main/java/ai/nubase/common/multitenancy/UnifiedMultiTenancyFilter.java +++ b/src/main/java/ai/nubase/common/multitenancy/UnifiedMultiTenancyFilter.java @@ -7,6 +7,7 @@ import ai.nubase.auth.service.JwtSecretService; import ai.nubase.auth.service.OAuthStateService; import ai.nubase.common.context.MultiTenancyContext; +import ai.nubase.common.util.SensitiveHeaderLogMask; import ai.nubase.common.enums.DatabaseInitStatus; import ai.nubase.common.enums.Role; import ai.nubase.postgrest.multidb.DatabaseConfig; @@ -492,20 +493,11 @@ private void logRequest(ContentCachingRequestWrapper request) { byte[] content = request.getContentAsByteArray(); String body = content.length > 0 ? new String(content, StandardCharsets.UTF_8) : null; - MCP_LOG.info("MCP Request: method={}, uri={}, headers={}, body={}", request.getMethod(), request.getRequestURI(), getHeaders(request), body); + MCP_LOG.info("MCP Request: method={}, uri={}, headers={}, body={}", + request.getMethod(), request.getRequestURI(), SensitiveHeaderLogMask.collectMasked(request), body); } - private Map getHeaders(HttpServletRequest request) { - Map headers = new HashMap<>(); - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String name = headerNames.nextElement(); - headers.put(name, request.getHeader(name)); - } - return headers; - } - private void logResponse(ContentCachingResponseWrapper response, long duration) { byte[] content = response.getContentAsByteArray(); String body = content.length > 0 ? new String(content, StandardCharsets.UTF_8) : null; diff --git a/src/main/java/ai/nubase/common/util/SensitiveHeaderLogMask.java b/src/main/java/ai/nubase/common/util/SensitiveHeaderLogMask.java new file mode 100644 index 0000000..99688bb --- /dev/null +++ b/src/main/java/ai/nubase/common/util/SensitiveHeaderLogMask.java @@ -0,0 +1,63 @@ +package ai.nubase.common.util; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * MCP/HTTP 请求日志用 header 脱敏:保留 header 名便于排障,敏感值仅留首尾片段。 + */ +public final class SensitiveHeaderLogMask { + + private static final Set SENSITIVE_NAMES = Set.of( + "apikey", + "authorization", + "cookie", + "proxy-authorization", + "x-api-key", + "x-auth-token" + ); + + private SensitiveHeaderLogMask() {} + + /** + * 从请求收集 header 并脱敏,专用于日志输出,不影响实际鉴权链路。 + */ + public static Map collectMasked(HttpServletRequest request) { + if (request == null) { + return Collections.emptyMap(); + } + Map headers = new LinkedHashMap<>(); + Enumeration names = request.getHeaderNames(); + if (names == null) { + return headers; + } + while (names.hasMoreElements()) { + String name = names.nextElement(); + headers.put(name, maskValue(name, request.getHeader(name))); + } + return headers; + } + + static String maskValue(String headerName, String value) { + if (!isSensitive(headerName)) { + return value; + } + if (value == null || value.isBlank()) { + return value; + } + if (value.length() <= 8) { + return "***"; + } + return value.substring(0, 4) + "..." + value.substring(value.length() - 4); + } + + private static boolean isSensitive(String headerName) { + return headerName != null && SENSITIVE_NAMES.contains(headerName.toLowerCase(Locale.ROOT)); + } +} diff --git a/src/test/java/ai/nubase/common/util/SensitiveHeaderLogMaskTest.java b/src/test/java/ai/nubase/common/util/SensitiveHeaderLogMaskTest.java new file mode 100644 index 0000000..17b9792 --- /dev/null +++ b/src/test/java/ai/nubase/common/util/SensitiveHeaderLogMaskTest.java @@ -0,0 +1,30 @@ +package ai.nubase.common.util; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +class SensitiveHeaderLogMaskTest { + + @Test + void collectMasked_redactsApikeyAndAuthorization() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("apikey", "eyJhbGciOiJIUzI1NiJ9.service_role_payload"); + request.addHeader("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.user_jwt"); + request.addHeader("Content-Type", "application/json"); + + var masked = SensitiveHeaderLogMask.collectMasked(request); + + assertThat(masked.get("apikey")).isEqualTo("eyJh...load"); + assertThat(masked.get("Authorization")).isEqualTo("Bear..._jwt"); + assertThat(masked.get("Content-Type")).isEqualTo("application/json"); + } + + @Test + void maskValue_shortSensitiveHeaderReturnsPlaceholder() { + assertThat(SensitiveHeaderLogMask.maskValue("apikey", "short")).isEqualTo("***"); + assertThat(SensitiveHeaderLogMask.maskValue("Content-Type", "application/json")) + .isEqualTo("application/json"); + } +}