fix(security): close S3 (arg-injection), S4 (presence-name), S1 (panic-slice)
Three findings from the first security pass: - S3 (Medium): the peer-supplied screen-share ticket was passed to pixelpass as the first positional CLI arg with no end-of-options guard, so a ticket starting with `-`/`--` could be reinterpreted as a flag (argument injection). New pure `viewer_args()` puts flags first, then a `--` guard, then the ticket positionally; spawn_viewer uses it. +2 tests. - S4 (Medium): peer presence display-names (gossip `Announce`, untrusted and spoofable) were rendered unsanitized/unbounded, unlike the chat path. New `sanitize::sanitize_name` strips bidi/zero-width format chars + control chars, collapses whitespace, and caps at 48 chars; applied at the gossip ingest point so every consumer gets a safe value. +4 tests. - S1 (Low): `&id[..8]` byte-slices could panic on a short/non-ASCII id. New panic-free `short_id()` (char-based take) replaces both slices. +1 test. 158 lib tests (was 151), clippy --all-targets clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+22
-2
@@ -871,6 +871,14 @@ fn format_duration(total_secs: u64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// First 8 characters of an id string for compact display. Panic-free: takes
|
||||
/// chars (not a byte slice), so a short or non-ASCII id can never panic the
|
||||
/// render (security finding S1) — ids are long ASCII hex today, but this guards
|
||||
/// the slice regardless.
|
||||
fn short_id(id: &str) -> String {
|
||||
id.chars().take(8).collect()
|
||||
}
|
||||
|
||||
/// Max characters kept for a single chat message after sanitizing.
|
||||
const CHAT_MSG_MAX_CHARS: usize = 2000;
|
||||
|
||||
@@ -1420,7 +1428,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
container(text("")).padding(0)
|
||||
},
|
||||
horizontal_space(),
|
||||
text(format!("My ID: {}", &state.self_id[..8]))
|
||||
text(format!("My ID: {}", short_id(&state.self_id)))
|
||||
.size(14)
|
||||
.color(color_subtext),
|
||||
button(
|
||||
@@ -1596,7 +1604,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
row![
|
||||
column![
|
||||
text(&peer.name).size(16).color(color_text),
|
||||
text(format!("ID: {}", &peer_id.to_string()[..8])).size(11).color(color_subtext)
|
||||
text(format!("ID: {}", short_id(&peer_id.to_string()))).size(11).color(color_subtext)
|
||||
],
|
||||
horizontal_space(),
|
||||
share_el,
|
||||
@@ -2790,6 +2798,18 @@ mod tests {
|
||||
assert_eq!(sanitize_chat(&long).chars().count(), CHAT_MSG_MAX_CHARS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_id_is_panic_free_on_short_and_unicode_ids() {
|
||||
use super::short_id;
|
||||
// Normal long hex id → first 8 chars.
|
||||
assert_eq!(short_id("abcdef0123456789"), "abcdef01");
|
||||
// Shorter-than-8 id must not panic (the old `[..8]` slice would).
|
||||
assert_eq!(short_id("abc"), "abc");
|
||||
assert_eq!(short_id(""), "");
|
||||
// Multi-byte chars: take 8 *chars*, never split a byte boundary.
|
||||
assert_eq!(short_id("héllo 世界 more"), "héllo 世界");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divider_clamps_are_finite_on_a_tiny_window() {
|
||||
// A window smaller than the reserves must not produce NaN/inverted ranges.
|
||||
|
||||
Reference in New Issue
Block a user