Skip to content

Strip Cf bidi/format controls in the terminal-bound error sanitizers (Trojan Source residual) #183

Description

@beardthelion

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_msgU+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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    crate:git-remotegit-remote-gitlawb — the git remote helperkind:securityVulnerability fix or hardeningsev:mediumDegraded but workaround existssubsystem:apiNode REST API request/response surfacesubsystem:visibilityPath-scoped visibility and content withholding

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions