Skip to content

gl doctor prints the node URL unsanitized, so ANSI and bidi characters from --node reach the terminal #393

Description

@beardthelion

Summary

gl doctor prints the node URL into three of its result rows without sanitizing it. The URL comes from --node or GITLAWB_NODE, so ANSI escapes, BEL, and a Unicode right-to-left override embedded in that value reach the operator's terminal verbatim. crates/gl/src/doctor.rs calls no sanitizer at all, while sync.rs, clone.rs, peer.rs, ipfs_cmd.rs and whoami.rs all route node-supplied strings through sanitize_node_msg.

This is the same terminal-injection class as #123 (read side) and #187 (write side), but a different input: those two are about the node's error message, this is about the node URL the user was handed. #187's inventory of 44 bail! sites does not include doctor, and doctor surfaces the value on its success path too, not only on failure.

Where

On main, crates/gl/src/doctor.rs:

  • :178 the node pass row, format!("{} — v{version} ({short_did}…)", args.node)
  • :197 the non-2xx row, format!("{} returned HTTP {}", args.node, resp.status())
  • :204 the unreachable row, format!("{} unreachable: {e}", args.node)

Reproduced by execution

Built gl and ran doctor with a --node value carrying ESC, BEL, and U+202E, then read the raw bytes of the output:

$ gl doctor --node $'http://127.0.0.1:1/\x1b[31m\x07'
  ✗  node   http://127.0.0.1:1/<ESC>[31m<BEL> unreachable: GET http://127.0.0.1:1/<ESC>[31m<BEL>/

od -c on that line shows 033, \a and 342 200 256 present unmodified. The value appears twice per row, once from args.node and once inside the reqwest error's own URL.

Why it matters

Lower severity than #187, and worth saying so plainly: the value is one the user typed or exported rather than one a node chose, so the delivery path is a copy-pasted install command, a README in an untrusted repo, or a shell profile someone else wrote, not a hostile node answering a request. What makes it worth closing anyway is that doctor is the command a confused user runs, its whole job is to be believed, and a right-to-left override can reorder a printed URL so the host reads as something else. Doctor also prints on success, so unlike the bail! sites there is no failure required to reach the sink.

Fix direction

Route all three through sanitize_node_msg (already pub(crate) in crates/gl/src/http.rs:325; it strips control characters and bidi format characters and caps at 200). Bind it once at the top of run rather than at each site, since the same value feeds every row. The {e} in the unreachable arm needs the same treatment, since the reqwest error embeds the URL again.

Two adjacent things in the same rows are node-supplied rather than caller-supplied and should go through the same helper while it is open: version and did at :178, read straight from the probed node's JSON body.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:securityVulnerability fix or hardeningsev:highMajor break or real security/trust risk, no easy workaround

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions