Summary
Across gl, node HTTP-response fields are extracted (value["field"].as_str()) and passed straight to println!/bail! with no sanitizer. A Cf bidi/format control (e.g. U+202E RLO) in an attacker-influenceable node field therefore reaches the operator's terminal and can visually reorder the line (Trojan Source, CWE-451), and a raw Cc escape (e.g. ESC) is equally unstripped.
This is the raw-print sibling of #183. #183 hardened the two error-body sanitizer functions; these sites have no sanitizer at all, so the is_control/sanitize-shaped grep used for #183 never surfaced them.
Affected (node field -> terminal, no sanitizer)
crates/gl/src/status.rs — node version (:71), open-PR title (:98), open-issue title (:125), all via println!.
crates/gl/src/bounty.rs — bounty-list title (:251, println!); node message on create/show/cancel failure (:197, :267, :377, bail!); cmd_show prints the entire node body pretty-printed (:270), which carries title/description fields raw.
crates/gl/src/agent.rs — agent model and capabilities (:105, :107, println!); node message on list/show failure (:76, :136, bail!).
crates/gl/src/quickstart.rs — node message on registration / repo-creation failure (:122, :208, println!).
(bounty.rs:202 echoes the caller's own --title, not node data, so it is excluded.)
None of these call gl::sync::sanitize_node_msg, which is the existing helper that strips !c.is_control() && !gitlawb_core::sanitize::is_bidi_format(c) and caps length (INV-6).
Reachability
The bytes are attacker-influenceable: a hostile or MITM'd node populates title / message / model / version in its JSON response, and they render to the operator's TTY during ordinary gl status / gl bounty / gl agent / gl quickstart use. No auth or special state required beyond talking to the node.
Reproduction (executed)
Driving the real bounty::cmd_show against a mock node returning {"message":"not<U+202E>found"} yields a terminal-bound error string of bounty not found: not<U+202E>found — the override survives. Separately, serde_json parse plus the exact ["title"].as_str() extraction the println! sites use preserves the U+202E it is handed. Both confirmed by test against the current tree.
Fix
Route every node-supplied string through a terminal sanitizer before printing. The pieces already exist from #183: the shared predicate gitlawb_core::sanitize::is_bidi_format and the sanitize_node_msg shape (strip Cc + the Cf reordering subset, bound length). Options, in order of preference:
- Promote a small
sanitize_node_msg-style helper to a shared location (it currently lives in gl::sync) and call it at each print site.
- Or inline the
!is_control() && !is_bidi_format() filter at each site.
Strip only the bidi-reordering subset (as #183 does); legitimate Cf such as ZWJ must pass through. Add a per-site (or per-command) test that a U+202E in the relevant node field does not survive to the printed/bailed string, mirroring the #183 tests.
Scope
In scope: the gl raw-print sites above. Out of scope: homoglyph and other Trojan-Source vectors beyond bidi reordering; node-side changes.
Severity
Same class and blast radius as #183: terminal spoofing/phishing, not a leak or RCE, with a known localized fix. Suggested sev:medium, kind:security, crate:gl.
Follow-up to #183 (PR #188).
Summary
Across
gl, node HTTP-response fields are extracted (value["field"].as_str()) and passed straight toprintln!/bail!with no sanitizer. ACfbidi/format control (e.g. U+202E RLO) in an attacker-influenceable node field therefore reaches the operator's terminal and can visually reorder the line (Trojan Source, CWE-451), and a rawCcescape (e.g. ESC) is equally unstripped.This is the raw-print sibling of #183. #183 hardened the two error-body sanitizer functions; these sites have no sanitizer at all, so the
is_control/sanitize-shaped grep used for #183 never surfaced them.Affected (node field -> terminal, no sanitizer)
crates/gl/src/status.rs— nodeversion(:71), open-PRtitle(:98), open-issuetitle(:125), all viaprintln!.crates/gl/src/bounty.rs— bounty-listtitle(:251,println!); nodemessageon create/show/cancel failure (:197,:267,:377,bail!);cmd_showprints the entire node body pretty-printed (:270), which carries title/description fields raw.crates/gl/src/agent.rs— agentmodelandcapabilities(:105,:107,println!); nodemessageon list/show failure (:76,:136,bail!).crates/gl/src/quickstart.rs— nodemessageon registration / repo-creation failure (:122,:208,println!).(
bounty.rs:202echoes the caller's own--title, not node data, so it is excluded.)None of these call
gl::sync::sanitize_node_msg, which is the existing helper that strips!c.is_control() && !gitlawb_core::sanitize::is_bidi_format(c)and caps length (INV-6).Reachability
The bytes are attacker-influenceable: a hostile or MITM'd node populates
title/message/model/versionin its JSON response, and they render to the operator's TTY during ordinarygl status/gl bounty/gl agent/gl quickstartuse. No auth or special state required beyond talking to the node.Reproduction (executed)
Driving the real
bounty::cmd_showagainst a mock node returning{"message":"not<U+202E>found"}yields a terminal-bound error string ofbounty not found: not<U+202E>found— the override survives. Separately,serde_jsonparse plus the exact["title"].as_str()extraction theprintln!sites use preserves the U+202E it is handed. Both confirmed by test against the current tree.Fix
Route every node-supplied string through a terminal sanitizer before printing. The pieces already exist from #183: the shared predicate
gitlawb_core::sanitize::is_bidi_formatand thesanitize_node_msgshape (stripCc+ theCfreordering subset, bound length). Options, in order of preference:sanitize_node_msg-style helper to a shared location (it currently lives ingl::sync) and call it at each print site.!is_control() && !is_bidi_format()filter at each site.Strip only the bidi-reordering subset (as #183 does); legitimate
Cfsuch as ZWJ must pass through. Add a per-site (or per-command) test that a U+202E in the relevant node field does not survive to the printed/bailed string, mirroring the #183 tests.Scope
In scope: the
glraw-print sites above. Out of scope: homoglyph and other Trojan-Source vectors beyond bidi reordering; node-side changes.Severity
Same class and blast radius as #183: terminal spoofing/phishing, not a leak or RCE, with a known localized fix. Suggested
sev:medium,kind:security,crate:gl.Follow-up to #183 (PR #188).