Skip to content

gl panics on a short or multi-byte timestamp from the node: 8 unguarded fixed-width slices across 5 commands #350

Description

@beardthelion

gl truncates node-supplied timestamps with a fixed byte index and no bounds check. The reported
site, crates/gl/src/issue.rs:244-247:

let created = issue["created_at"]
    .as_str()
    .map(|s| &s[..10])
    .unwrap_or("?");

unwrap_or("?") covers a missing or non-string field and does nothing for a present-but-short one,
which is the actual trap. issue comes straight from the raw .json() of the response with no
schema validation in between.

Seven siblings, same shape: issue.rs:380, pr.rs:376, pr.rs:533, repo.rs:299, peer.rs:89
(last_seen), and cert.rs:123 (&s[..19] on issued_at).

Two failure modes, and the existing guards only cover one

Rust &str slicing panics both when the index is out of range and when it lands inside a multi-byte
character. Executed with the expressions copied verbatim:

input &s[..10] (unguarded) &s[..10.min(s.len())] (the existing "guard")
"2026-08-15T12:34:56Z" (control) ok 2026-08-15 ok
"2026" PANIC, out of bounds ok
"" PANIC, out of bounds ok
"2026-08-1é5T12:34:56Z" PANIC, not a char boundary PANIC, not a char boundary

That last column is the part worth flagging. repo.rs:498, repo.rs:555, and node.rs:404 already
use .min(s.len()), and ipfs_cmd.rs:76 uses an if p.len() >= 19 guard. Both fix the
out-of-bounds mode and neither fixes the char-boundary mode, so a fix that sweeps the unguarded sites
into the existing pattern would not actually close this.

Reachability

gl issue list, gl pr list, gl pr show, gl repo list, gl peer list, gl cert list against a
hostile or MITM'd node. crates/gl/src/http.rs performs no response validation, and on the read path
get_authed falls back to an unsigned GET when no keypair is present, so the response is
unauthenticated. The client already treats the node as untrusted (that is the premise of #303, #189,
and #187).

A one-character created_at produces an unhandled panic with a backtrace and a non-zero exit; there
is no catch_unwind or custom panic hook anywhere in crates/gl/.

Severity

Low. It is reachable and it is a crash, but the blast radius is one CLI process on the user's own
machine, on a read-only display path, with no memory unsafety, no authorization consequence, and no
server impact. A hostile node can already just refuse to answer. What earns it an issue is breadth
(8 sites, 5 commands) plus the fact that the three existing guards are wrong.

Fix direction

One shared helper that truncates on a char boundary, called from all 11 sites (the 8 unguarded and
the 3 mis-guarded), rather than a per-site .min(len) sweep. s.char_indices().nth(n) or
floor_char_boundary both work; the former is stable today.

Related but separate: cmd_list in issue.rs never checks resp.status() before parsing, while
cmd_show directly below it does. That is the #123 class rather than this one, and PR #186 is the
place for it.

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

    crate:glgl — the contributor CLIkind:bugDefect fix — wrong or unsafe behaviorsev:lowCosmetic, cleanup, or nice-to-havesubsystem:apiNode REST API request/response surfacesubsystem:attestationCertificates, anchoring, per-ref attestationsubsystem:peersPeer announce, discovery, and registry

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions