fix(L3): redact key material from crash reports before persistence (GHSA-8844-q5vh-9j8f)

ReportAssembler previously appended e.toString()/cause.toString() and the
full stack trace verbatim. Parser/crypto exceptions can embed attacker-
controlled request data (hex keys, npub/nsec bech32, base64 ciphertexts).
Add a redaction step that replaces 64-char hex keys, Nostr bech32 prefixes
and long base64 blobs with placeholders before the report is written to
internal storage by UnexpectedCrashSaver.

Adds ReportAssemblerTest covering hex/bech32/base64/cause-chain redaction
and non-sensitive content preservation.
This commit is contained in:
greenart7c3
2026-08-14 07:07:19 -03:00
parent ee975e9833
commit e78d0d651f
2 changed files with 89 additions and 4 deletions
@@ -71,22 +71,46 @@ class ReportAssembler {
appendLine(" |")
appendLine()
// Defense-in-depth (GHSA-8844-q5vh-9j8f, L3): exception messages and
// stack traces can embed attacker-controlled request data (parser/
// crypto exceptions quoting bad input). Redact Nostr key material
// (bech32 npub/nsec/etc, 64-char hex keys) and long base64 blobs
// before persistence so the crash report cannot leak secrets.
appendLine("```")
appendLine(e.toString())
appendLine(redactSensitive(e.toString()))
e.stackTrace.forEach {
append(" ")
appendLine(it.toString())
appendLine(redactSensitive(it.toString()))
}
val cause = e.cause
if (cause != null) {
appendLine("\n\nCause:")
append(" ")
appendLine(cause.toString())
appendLine(redactSensitive(cause.toString()))
cause.stackTrace.forEach {
append(" ")
appendLine(it.toString())
appendLine(redactSensitive(it.toString()))
}
}
appendLine("```")
}
private fun redactSensitive(input: String): String = input
.replace(BECH32_REDACT, "<bech32>")
.replace(HEXKEY_REDACT, "<hexkey>")
.replace(BASE64_REDACT, "<base64>")
private companion object {
// bech32 prefixes used by Nostr (npub, nsec, note, nprofile, nevent,
// naddr, nrelay, ncryptsec). Match the hrp + data part; bech32 uses
// the alphabet 023456789acdefghjklmnpqrstuvwxyz (no b, i, o, 1).
private val BECH32_REDACT =
Regex("\\b(npub|nsec|note|nprofile|nevent|naddr|nrelay|ncryptsec)1[023456789acdefghjklmnpqrstuvwxyz]{6,}\\b")
// 64-char lowercase hex keys (pubkeys/event ids).
private val HEXKEY_REDACT = Regex("\\b[0-9a-f]{64}\\b")
// Long base64 runs (>=32 chars): NIP-44/44v3 ciphertexts, payloads.
private val BASE64_REDACT = Regex("\\b[A-Za-z0-9+/]{32,}={0,2}\\b")
}
}
@@ -0,0 +1,61 @@
package com.greenart7c3.nostrsigner.service.crashreports
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ReportAssemblerTest {
private val npubHex = "3bf0c63fcb93463407af97a5e5ee64b88f1f2c73b1a21c4f33ac8f0c7c6a6e0b"
@Test
fun `redacts 64-char hex keys from exception messages and stack traces`() {
val e = IllegalStateException("bad key $npubHex in input")
val report = ReportAssembler().buildReport(e)
assertFalse("hex key must be redacted", report.contains(npubHex))
assertTrue("redaction marker present", report.contains("<hexkey>"))
}
@Test
fun `redacts npub bech32 from exception messages`() {
// npub1... derived from the hex key above
val npub = "npub10qp7qrzdrqmax40ayhxm2vp6d8yurfr3lwxyh4df0sf5r8cjnrms2cv7aj"
val e = IllegalArgumentException("could not parse $npub")
val report = ReportAssembler().buildReport(e)
assertFalse("npub must be redacted", report.contains(npub))
assertTrue("redaction marker present", report.contains("<bech32>"))
}
@Test
fun `redacts long base64 blobs from exception messages`() {
val base64 = "kL9/abcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
val e = RuntimeException("decryption failed for payload=$base64")
val report = ReportAssembler().buildReport(e)
assertFalse("base64 must be redacted", report.contains(base64))
assertTrue("redaction marker present", report.contains("<base64>"))
}
@Test
fun `preserves non-sensitive message content`() {
val e = IllegalStateException("null pointer in feature X")
val report = ReportAssembler().buildReport(e)
assertTrue("non-sensitive content preserved", report.contains("null pointer in feature X"))
}
@Test
fun `redacts cause chain too`() {
val cause = IllegalArgumentException("bad hex $npubHex")
val e = RuntimeException("wrapper", cause)
val report = ReportAssembler().buildReport(e)
assertFalse("cause hex must be redacted", report.contains(npubHex))
}
}