Merge pull request #3656 from davotoula/fix/bidi-control-literals

Replace raw bidi/invisible Unicode characters in source with \u escapes
This commit is contained in:
Vitor Pamplona
2026-07-21 10:53:49 -04:00
committed by GitHub
4 changed files with 21 additions and 6 deletions
+15
View File
@@ -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+202AU+202E, the isolates U+2066U+2069, U+200E/U+200F, U+061C —
plus zero-width characters (U+200BU+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
@@ -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
@@ -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+")
@@ -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+")