Summary
Two terminal-bound error sanitizers strip only Unicode Cc (control) characters and let Cf (bidi/format) characters through, so a right-to-left override in attacker-influenceable bytes survives to the terminal and can visually reorder the displayed line (Trojan Source, CWE-451). The gl client already handles this class; these two do not.
Affected
git-remote-gitlawb::safe_error_body_excerpt (crates/git-remote-gitlawb/src/main.rs:556, filter at :566) — runs on the untrusted HTTP error body from the node (main.rs:543) and reaches the terminal via the error path.
icaptcha-client::sanitize_excerpt (crates/icaptcha-client/src/lib.rs:53, filter at :61) — runs on the service-supplied reason/body from a node-advertised iCaptcha origin and is bail!'d to the terminal (lib.rs:295).
Both filter !c.is_control() only. char::is_control() matches category Cc (C0/C1 + DEL); it does not match Cf (bidi overrides/isolates: U+202A–202E, U+2066–2069, U+200E/200F/061C).
Reference impl (correct)
gl::sanitize_node_msg (crates/gl/src/sync.rs:135) filters !c.is_control() && !is_bidi_format(c), where is_bidi_format (sync.rs:144) covers the Cf ranges. That is the shape to port.
Reproduction (executed)
Each function was called with "error: \u{202e}drowssap\u{202c} ok":
safe_error_body_excerpt → output still contains U+202E (FAIL)
sanitize_excerpt → output still contains U+202E (FAIL)
sanitize_node_msg → U+202E stripped (PASS)
The Cc class (ESC/BEL/NUL) is stripped correctly in all three; only the Cf residual remains in the first two.
Reachability
The bytes are attacker-influenceable (a hostile or MITM'd node's error body; an attacker-selectable or compromised iCaptcha service response) and render to the operator's TTY during a normal git operation. The dangerous escape-introducer (Cc) class is already neutralized, so this is the residual bidi/format vector, not the full escape-injection one.
Fix
Add an is_bidi_format predicate (port gl's) and extend both filters to !c.is_control() && !is_bidi_format(c). Add a per-crate test asserting U+202E is stripped. Note that icaptcha-client's existing sanitize_strips_control_chars_and_escapes test asserts only that is_control() chars are gone, which is why the gap survived — the new test must assert the Cf case specifically.
Severity
Hardening completeness of an already-partially-mitigated terminal-injection vector: bounded blast radius (visual spoofing/phishing, not leak or RCE), known one-function fix. Suggested sev:medium.
Related: the INV-6 terminal-escape class; the prior gl fix in #82 that added the bidi predicate there.
Summary
Two terminal-bound error sanitizers strip only Unicode
Cc(control) characters and letCf(bidi/format) characters through, so a right-to-left override in attacker-influenceable bytes survives to the terminal and can visually reorder the displayed line (Trojan Source, CWE-451). Theglclient already handles this class; these two do not.Affected
git-remote-gitlawb::safe_error_body_excerpt(crates/git-remote-gitlawb/src/main.rs:556, filter at:566) — runs on the untrusted HTTP error body from the node (main.rs:543) and reaches the terminal via the error path.icaptcha-client::sanitize_excerpt(crates/icaptcha-client/src/lib.rs:53, filter at:61) — runs on the service-suppliedreason/body from a node-advertised iCaptcha origin and isbail!'d to the terminal (lib.rs:295).Both filter
!c.is_control()only.char::is_control()matches categoryCc(C0/C1 + DEL); it does not matchCf(bidi overrides/isolates:U+202A–202E,U+2066–2069,U+200E/200F/061C).Reference impl (correct)
gl::sanitize_node_msg(crates/gl/src/sync.rs:135) filters!c.is_control() && !is_bidi_format(c), whereis_bidi_format(sync.rs:144) covers theCfranges. That is the shape to port.Reproduction (executed)
Each function was called with
"error: \u{202e}drowssap\u{202c} ok":safe_error_body_excerpt→ output still containsU+202E(FAIL)sanitize_excerpt→ output still containsU+202E(FAIL)sanitize_node_msg→U+202Estripped (PASS)The
Ccclass (ESC/BEL/NUL) is stripped correctly in all three; only theCfresidual remains in the first two.Reachability
The bytes are attacker-influenceable (a hostile or MITM'd node's error body; an attacker-selectable or compromised iCaptcha service response) and render to the operator's TTY during a normal git operation. The dangerous escape-introducer (
Cc) class is already neutralized, so this is the residual bidi/format vector, not the full escape-injection one.Fix
Add an
is_bidi_formatpredicate (portgl's) and extend both filters to!c.is_control() && !is_bidi_format(c). Add a per-crate test assertingU+202Eis stripped. Note thaticaptcha-client's existingsanitize_strips_control_chars_and_escapestest asserts only thatis_control()chars are gone, which is why the gap survived — the new test must assert theCfcase specifically.Severity
Hardening completeness of an already-partially-mitigated terminal-injection vector: bounded blast radius (visual spoofing/phishing, not leak or RCE), known one-function fix. Suggested
sev:medium.Related: the INV-6 terminal-escape class; the prior
glfix in #82 that added the bidi predicate there.