diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 7225a48c67..e0a8c7604a 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -302,6 +302,21 @@ Do this before considering the task complete. `forEach`/`map`/`filter`/`any` — the `fast*` variants allocate no iterator, no intermediate list, and no lambda object. Don't "modernize" those into stdlib collection calls; match the surrounding hot-path style. +- **Never put raw invisible/bidirectional Unicode characters in source files** + — write them as `\uXXXX` escapes instead (`'\u202E'`, `Regex("[\u200B-\u200D\uFEFF]")`). + This covers the bidi family Sonar's Trojan-Source rule (CVE-2021-42574) + flags — U+202A–U+202E, the isolates U+2066–U+2069, U+200E/U+200F, U+061C — + plus zero-width characters (U+200B–U+200D, U+FEFF, U+2060). The escape + compiles to the identical codepoint, so behaviour is unchanged; the point is + that the file on disk stays visually unambiguous. Applies even when the + character is *intentional* (sanitizer strip-lists, adversarial test + payloads) — that's data, and escapes express it just as well. Exceptions: + U+200D as part of a real emoji ZWJ sequence in test data (👩‍👧 — functional, + not a bidi control), and LRM/RLM inside Crowdin-managed `strings.xml` + translations (legitimate RTL typography; don't touch those files by hand + anyway). Note the tooling trap: the Edit tool may normalise a typed + `\uXXXX` back into the raw character — if that happens, do the replacement + at byte level (`perl -CSD -pe 's/\x{202E}/\\u202E/g'`). ### Navigation Shell - **Desktop**: Sidebar + main content area diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomPaymentSafetyTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomPaymentSafetyTest.kt index d1564aeac8..fbaed1939b 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomPaymentSafetyTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomPaymentSafetyTest.kt @@ -215,9 +215,9 @@ class BlossomPaymentSafetyTest { @Test fun reasonBidiOverridesAreRemoved() { - val clean = challenge(invoice1000Sats, reason = "fee ‮reversed‬ text").sanitizedReason()!! - assertFalse(clean.contains('‮')) - assertFalse(clean.contains('‬')) + val clean = challenge(invoice1000Sats, reason = "fee \u202Ereversed\u202C text").sanitizedReason()!! + assertFalse(clean.contains('\u202E')) + assertFalse(clean.contains('\u202C')) } @Test diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/Sanitizer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/Sanitizer.kt index d2fd4927fc..bd6cc3098b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/Sanitizer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/Sanitizer.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst.commons.moderation.notifications private val CONTROL_CHARS = Regex("\\p{Cntrl}") -private val RTL_OVERRIDES = Regex("[‪-‮⁦-⁩]") -private val ZERO_WIDTH = Regex("[​-‍]") +private val RTL_OVERRIDES = Regex("[\u202A-\u202E\u2066-\u2069]") +private val ZERO_WIDTH = Regex("[\u200B-\u200D\uFEFF]") private val WHITESPACE = Regex("\\s+") private val URL_PATTERN = Regex("https?://\\S+") diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomPaymentRequired.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomPaymentRequired.kt index 6ffd47cb8c..d1adbc8938 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomPaymentRequired.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomPaymentRequired.kt @@ -74,7 +74,7 @@ data class BlossomPaymentRequired( const val MAX_REASON_LENGTH = 200 /** LRE/RLE/PDF/LRO/RLO and the isolate family — invisible, and they reorder what follows. */ - private val BIDI_OVERRIDES = charArrayOf('‪', '‫', '‬', '‭', '‮', '⁦', '⁧', '⁨', '⁩', '‏', '‎') + private val BIDI_OVERRIDES = charArrayOf('\u202A', '\u202B', '\u202C', '\u202D', '\u202E', '\u2066', '\u2067', '\u2068', '\u2069', '\u200F', '\u200E') private val WHITESPACE_RUN = Regex("\\s+")