审计日志要回答“谁在什么时候做了什么”,不能把秘密也记下来,也不能被悄悄改掉。这篇用真实运行量出:JDK 自带的调试日志与日常的字符串会泄露什么,哈希链能防什么、防不住什么,以及 JFR 里的安全事件长什么样。
[默认(没有调试)] 输出 1 行,其中十六进制转储的字节 0 个;直接搜索文本:“SECRET-TOKEN-123” 出现 0 次;把所有十六进制转储拼起来再搜索:“SECRET-TOKEN-123” 0 次,“hunter2” 0 次,“Authorization” 0 次 [-Djavax.net.debug=ssl:handshake] 输出 4922 行,其中十六进制转储的字节 3931 个;直接搜索文本:“SECRET-TOKEN-123” 出现 0 次;把所有十六进制转储拼起来再搜索:“SECRET-TOKEN-123” 0 次,“hunter2” 0 次,“Authorization” 0 次 [-Djavax.net.debug=ssl:record] 输出 4922 行,其中十六进制转储的字节 3931 个;直接搜索文本:“SECRET-TOKEN-123” 出现 0 次;把所有十六进制转储拼起来再搜索:“SECRET-TOKEN-123” 0 次,“hunter2” 0 次,“Authorization” 0 次 [-Djavax.net.debug=ssl:record:plaintext] 输出 5387 行,其中十六进制转储的字节 10926 个;直接搜索文本:“SECRET-TOKEN-123” 出现 0 次;把所有十六进制转储拼起来再搜索:“SECRET-TOKEN-123” 1 次,“hunter2” 1 次,“Authorization” 1 次 [-Djavax.net.debug=all] 输出 5937 行,其中十六进制转储的字节 18718 个;直接搜索文本:“SECRET-TOKEN-123” 出现 0 次;把所有十六进制转储拼起来再搜索:“SECRET-TOKEN-123” 1 次,“hunter2” 1 次,“Authorization” 1 次
URL.toString():https://alice:S3cret!pw@example.com/path?x=1
URI.toString():https://alice:S3cret!pw@example.com/path?x=1
HttpRequest.toString():https://alice:S3cret!pw@example.com/path?x=1 GET
URL.getAuthority() = alice:S3cret!pw@example.com,getUserInfo() = alice:S3cret!pw,getHost() = example.com(只有 getHost() 不含密码)
Properties.toString():{db.user=alice, db.password=S3cret!pw}
System.out.println(char[]) 打印:S3cret!pw
println(Object) 打印 char[]:[C@7cca494b(只是类型与哈希);Arrays.toString(char[]):[S, 3, c, r, e, t, !, p, w];String.valueOf(char[]):S3cret!pw
sun.java.command 系统属性(包含启动参数):Main leaks --password=cli-secret-42
ProcessHandle.current().info().commandLine():java -cp out Main leaks --password=cli-secret-42
Integer.parseInt(口令) 抛出:NumberFormatException: For input string: "S3cret!pw" → 包含口令 = true
new BigDecimal(口令) 抛出:NumberFormatException: Character S is neither a decimal digit number, decimal point, nor "e" notation exponential mark. → 包含口令 = false
UUID.fromString(口令) 抛出:IllegalArgumentException: Invalid UUID string: S3cret!pw → 包含口令 = true
URI.create("http://a b/" + 口令) 抛出:IllegalArgumentException: Illegal character in authority at index 8: http://a b/S3cret!pw → 包含口令 = true
Base64 解码含非法字符的口令 抛出:IllegalArgumentException: Illegal base64 character 21 → 包含口令 = false
LocalDate.parse(口令) 抛出:DateTimeParseException: Text 'S3cret!pw' could not be parsed at index 0 → 包含口令 = true
Enum.valueOf(口令) 抛出:IllegalArgumentException: No enum constant java.lang.Thread.State.S3cret!pw → 包含口令 = true
Cipher.getInstance 传入口令:NoSuchAlgorithmException: Cannot find any provider supporting AES/S3cret!pw/NoPadding → 包含口令 = true
另一个进程(同一用户)用 ps -o args= 看到的命令行:sh -c sleep 20; echo "$1" sh --token=ARGV-SECRET-777;“ARGV-SECRET-777” 出现 = True;在 ps eww 的输出里 “ENV-SECRET-888”(环境变量)出现 = False N = 1000 条审计记录,每条 = JSON 正文 | 上一条的哈希 | 本条哈希。示例第 1 条:{"seq":1,"actor":"user1","action":"read","resource":"/orders/1","outcome":"ok"}|00000000000000000000…
原始日志校验:全部通过
篡改第 500 条的正文(把 delete 改成 read,ok 改成 denied):校验失败的第一条 seq = 500
删除第 500 条:校验失败的第一条 seq = 500(原来第 501 条的上一条哈希对不上了)
交换第 100 与第 101 条:校验失败的第一条 seq = 100
删除最后 10 条(截断尾部):无锚点时校验 全部通过(没有发现);用外部保存的最后一条哈希作锚点时校验失败的位置 = 991(= 剩余条数 + 1)
攻击者从第 500 条起把后面的哈希全部重新计算(普通 SHA-256 链,不需要任何密钥):无锚点校验 全部通过(篡改成功);用外部锚点(原始的最后一条哈希)校验失败的位置 = 1001
同样的攻击,但日志用 HMAC-SHA256 链(密钥不在攻击者手里):攻击者用普通 SHA-256 重算后,用 HMAC 校验失败的第一条 seq = 500;原始 HMAC 日志校验 全部通过
HMAC 链:生成 1000 条约 9.2 ms(每条约 9.2 µs),校验 1000 条约 8.2 ms事件 jdk.TLSHandshake:1 个 事件 jdk.X509Validation:2 个 事件 jdk.X509Certificate:224 个 事件 jdk.SecurityPropertyModification:0 个 jdk.TLSHandshake 的内容:peerHost = "localhost" | peerPort = 9771 | protocolVersion = "TLSv1.3" | cipherSuite = "TLS_AES_256_GCM_SHA384" | certificateId = 1371337912 jdk.X509Validation 的内容:startTime = 19:01:43.772 (2026-10-05) | certificateId = 4248717943 | certificatePosition = 1 | validationCounter = 1 | startTime = 19:01:43.772 (2026-10-05) | certificateId = 1371337912 | certificatePosition = 2 | validationCounter = 1 jdk.X509Certificate 的内容(第一个事件):algorithm = "SHA256withECDSA" | subject = "CN=Local Test CA" | issuer = "CN=Local Test CA" | keyType = "EC" | keyLength = 256 | validFrom = 19:01:35.000 (2026-10-05) | validUntil = 19:01:35.000 (2026-10-07) 整个 JFR 文件打印出的文本里 “SECRET-TOKEN-123” 出现 0 次,“hunter2” 出现 0 次