Summary
The gl CLI and MCP write/mutation commands surface the node's error message to the terminal via anyhow::bail!("... ({status}): {msg}") without sanitizing it. A hostile or MITM node can embed ANSI escape sequences or a Unicode right-to-left override in that message; on a failed write it reaches the operator's terminal verbatim, enabling terminal spoofing (the ANSI-injection / Trojan Source class #183 addressed on the sync path). This is the write-side counterpart of the read-side fix in #123 / #186, which routes read arms through read_json and sanitizes via sanitize_node_msg. The crate is currently mixed: reads sanitized, writes raw.
Where
44 bail!("... {msg}") sites across crates/gl/src surface a raw node message; only one (repo.rs cmd_info, fixed in #186) sanitizes it. Representative write sites: repo.rs cmd_create, pr.rs cmd_create / merge / review / comment, bounty.rs cmd_create and the claim/submit/approve/cancel handlers, plus protect / star / visibility / profile / init / register.
Reproduced by execution
A throwaway test drove a hostile node message (a JSON message field carrying an ESC U+001B and a right-to-left override U+202E, supplied via JSON \u escapes) through gl repo create against a mock node returning 409, and asserted the surfaced error string still contained the raw ESC and RLO. It passed: the raw control bytes reach the terminal unsanitized. The read side, by contrast, strips them (read_json calls sanitize_node_msg).
Why it matters
The node a gl user talks to is user-selectable (--node / GITLAWB_NODE, default node.gitlawb.com), so the error message is attacker-influenceable when a user is pointed at a hostile node or a MITM without cert pinning. ANSI/bidi terminal injection is an active, CVE'd class: Flawfinder's CVE-2026-48813 hides scan results via ANSI escapes in filenames, an ANSI-injection RCE was reported in the OpenAI Codex CLI, WinRAR had screen-spoofing CVEs, and the Trojan Source paper covers the RLO trick. #183 closed the sync-path residual; the write commands were left out of #123's read-only scope.
Fix direction
Route every write/mutation bail! that interpolates a node message through sanitize_node_msg (already pub(crate) in crates/gl/src/sync.rs), the same way read_json and cmd_info do. A small shared "extract and sanitize the node message from an error response" helper would DRY the ~43 remaining sites and make the correct pattern the default, mirroring how read_json did it for the read side.
Severity
Low. Terminal-display spoofing that requires the user to be pointed at a hostile or MITM node; no data leak or auth bypass. Same hardening class as #183; write-side counterpart of the read-side #123.
Summary
The
glCLI and MCP write/mutation commands surface the node's errormessageto the terminal viaanyhow::bail!("... ({status}): {msg}")without sanitizing it. A hostile or MITM node can embed ANSI escape sequences or a Unicode right-to-left override in that message; on a failed write it reaches the operator's terminal verbatim, enabling terminal spoofing (the ANSI-injection / Trojan Source class #183 addressed on the sync path). This is the write-side counterpart of the read-side fix in #123 / #186, which routes read arms throughread_jsonand sanitizes viasanitize_node_msg. The crate is currently mixed: reads sanitized, writes raw.Where
44
bail!("... {msg}")sites acrosscrates/gl/srcsurface a raw nodemessage; only one (repo.rscmd_info, fixed in #186) sanitizes it. Representative write sites:repo.rscmd_create,pr.rscmd_create / merge / review / comment,bounty.rscmd_create and the claim/submit/approve/cancel handlers, plus protect / star / visibility / profile / init / register.Reproduced by execution
A throwaway test drove a hostile node message (a JSON
messagefield carrying an ESCU+001Band a right-to-left overrideU+202E, supplied via JSON\uescapes) throughgl repo createagainst a mock node returning 409, and asserted the surfaced error string still contained the raw ESC and RLO. It passed: the raw control bytes reach the terminal unsanitized. The read side, by contrast, strips them (read_jsoncallssanitize_node_msg).Why it matters
The node a
gluser talks to is user-selectable (--node/GITLAWB_NODE, defaultnode.gitlawb.com), so the errormessageis attacker-influenceable when a user is pointed at a hostile node or a MITM without cert pinning. ANSI/bidi terminal injection is an active, CVE'd class: Flawfinder's CVE-2026-48813 hides scan results via ANSI escapes in filenames, an ANSI-injection RCE was reported in the OpenAI Codex CLI, WinRAR had screen-spoofing CVEs, and the Trojan Source paper covers the RLO trick. #183 closed the sync-path residual; the write commands were left out of #123's read-only scope.Fix direction
Route every write/mutation
bail!that interpolates a nodemessagethroughsanitize_node_msg(alreadypub(crate)incrates/gl/src/sync.rs), the same wayread_jsonandcmd_infodo. A small shared "extract and sanitize the node message from an error response" helper would DRY the ~43 remaining sites and make the correct pattern the default, mirroring howread_jsondid it for the read side.Severity
Low. Terminal-display spoofing that requires the user to be pointed at a hostile or MITM node; no data leak or auth bypass. Same hardening class as #183; write-side counterpart of the read-side #123.