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.
gltruncates node-supplied timestamps with a fixed byte index and no bounds check. The reportedsite,
crates/gl/src/issue.rs:244-247:unwrap_or("?")covers a missing or non-string field and does nothing for a present-but-short one,which is the actual trap.
issuecomes straight from the raw.json()of the response with noschema validation in between.
Seven siblings, same shape:
issue.rs:380,pr.rs:376,pr.rs:533,repo.rs:299,peer.rs:89(
last_seen), andcert.rs:123(&s[..19]onissued_at).Two failure modes, and the existing guards only cover one
Rust
&strslicing panics both when the index is out of range and when it lands inside a multi-bytecharacter. Executed with the expressions copied verbatim:
&s[..10](unguarded)&s[..10.min(s.len())](the existing "guard")"2026-08-15T12:34:56Z"(control)2026-08-15"2026""""2026-08-1é5T12:34:56Z"That last column is the part worth flagging.
repo.rs:498,repo.rs:555, andnode.rs:404alreadyuse
.min(s.len()), andipfs_cmd.rs:76uses anif p.len() >= 19guard. Both fix theout-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 listagainst ahostile or MITM'd node.
crates/gl/src/http.rsperforms no response validation, and on the read pathget_authedfalls back to an unsigned GET when no keypair is present, so the response isunauthenticated. The client already treats the node as untrusted (that is the premise of #303, #189,
and #187).
A one-character
created_atproduces an unhandled panic with a backtrace and a non-zero exit; thereis no
catch_unwindor custom panic hook anywhere incrates/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)orfloor_char_boundaryboth work; the former is stable today.Related but separate:
cmd_listinissue.rsnever checksresp.status()before parsing, whilecmd_showdirectly below it does. That is the #123 class rather than this one, and PR #186 is theplace for it.