Răsfoiți Sursa

fix(日志): 截断片段中的敏感子树与转义值遮盖

alphaH 1 săptămână în urmă
părinte
comite
753ffe6b16

+ 15 - 2
java/src/main/java/com/payment/platform/core/log/SensitivePayloadRedactor.java

@@ -69,9 +69,21 @@ public final class SensitivePayloadRedactor {
     /**
      * 截断点正好落在敏感值字符串内部时的尾部:值没有闭合引号,上面的成对规则匹配不到,
      * 这里把该键冒号后的整段尾巴(到文本末尾)全部盖上。
+     *
+     * <p>尾部用「转义感知」的 {@code (?:[^"\\]|\\.)*} 而不是 {@code [\s\S]*}:
+     * 后者会跨过后文里正常的闭合引号,把已经成对替换过的后续字段(含非敏感字段)一并吞掉,
+     * 破坏“只对敏感键动手”。转义感知版仍能吃掉含 {@code \"} 的未闭合值前缀
+     * (R30 的转义引号截断用例),但不会越过一个未转义的引号 —— 那意味着该值其实已闭合。
      */
     private static final Pattern JSON_UNTERMINATED = Pattern.compile(
-            "(?i)(\"(?:" + KEY_ALTERNATION + ")\"\\s*:\\s*\")[^\"]*$");
+            "(?i)(\"(?:" + KEY_ALTERNATION + ")\"\\s*:\\s*\")(?:[^\"\\\\]|\\\\.)*$");
+
+    /**
+     * 截断后的 JSON 片段里,敏感键的值是对象/数组(整体已不可解析):
+     * 从该 {@code &#123;} / {@code [} 一直遮盖到文本末尾(内层非敏感键也不得漏出)。
+     */
+    private static final Pattern JSON_CONTAINER_TAIL = Pattern.compile(
+            "(?i)(\"(?:" + KEY_ALTERNATION + ")\"\\s*:\\s*)[\\[{][\\s\\S]*$");
 
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
@@ -100,7 +112,8 @@ public final class SensitivePayloadRedactor {
         }
         String withoutQuoted = JSON_QUOTED.matcher(truncated).replaceAll("$1\"" + MASK + "\"");
         String withoutTail = JSON_UNTERMINATED.matcher(withoutQuoted).replaceAll("$1" + MASK);
-        return clamp(KEY_VALUE.matcher(withoutTail).replaceAll("$1=" + MASK));
+        String withoutContainer = JSON_CONTAINER_TAIL.matcher(withoutTail).replaceAll("$1" + MASK);
+        return clamp(KEY_VALUE.matcher(withoutContainer).replaceAll("$1=" + MASK));
     }
 
     /** 星号比原值长时(如 {@code "S1"} → {@code "***"}),最终长度仍不得超过上限。 */

+ 29 - 0
java/src/test/java/com/payment/platform/core/log/SensitivePayloadRedactorTest.java

@@ -143,6 +143,35 @@ class SensitivePayloadRedactorTest {
         assertTrue(out.contains("\"app_secret\":\"***"), out);
     }
 
+    @Test
+    void 截断在含转义引号的敏感值内部时_前缀也不落日志() {
+        String out = SensitivePayloadRedactor.redact(
+                "{\"channel_extra\":\"{\\\"openid\\\":\\\"o1secret");
+
+        assertFalse(out.contains("o1secret"), "未闭合的转义值前缀泄露了: " + out);
+        assertTrue(out.length() <= 2000, "实际长度 " + out.length());
+    }
+
+    @Test
+    void 敏感键后紧跟对象时_整棵子树盖到末尾() {
+        String out = SensitivePayloadRedactor.redact("{\"channelExtra\":{\"foo\":\"bar\"}");
+
+        assertFalse(out.contains("bar"), "敏感键的对象子树泄露了: " + out);
+        // 非敏感内层键不得漏出,敏感键名本身保留以证明是按键定位
+        assertFalse(out.contains("foo"), "敏感键的对象子树泄露了: " + out);
+        assertTrue(out.contains("channelExtra"), out);
+    }
+
+    @Test
+    void 合法JSON只对敏感键动手_非敏感值原样保留() throws Exception {
+        String out = SensitivePayloadRedactor.redact("{\"amount\":100,\"foo\":\"bar\"}");
+
+        assertTrue(out.contains("bar"), "合法 JSON 不应被兜底规则吞掉: " + out);
+        JsonNode node = MAPPER.readTree(out);
+        assertEquals(100, node.get("amount").asInt());
+        assertEquals("bar", node.get("foo").asText());
+    }
+
     @Test
     void 非JSON回退_转义的JSON字符串值整段变星号() {
         String out = SensitivePayloadRedactor.redact(