fix(security): land the 2026-07 audit fixes — SSRF guards, roster lane, SYSTEM path hygiene

The low/medium findings from the July host+Windows security review, as
implemented in the audit session's working tree:

- Webhooks and library-art fetches refuse loopback/link-local/metadata
  targets and no longer follow redirects (SSRF pivots from the privileged
  host process).
- The paired-client rosters (/clients, /native/clients) move off the
  streaming-client auth lane — one paired device could enumerate every other
  device's name + fingerprint; only the bearer/loopback console keeps them.
- Device-name sanitizing extends to bidi/format control characters
  (spoofable rendering) via the shared native_pairing::is_spoofy_char;
  stream-marker quoting uses the same set.
- The SYSTEM service resolves powershell by its full System32 path —
  CreateProcess checks the launching EXE's own directory first, so a planted
  powershell.exe beside the host binary would have run as SYSTEM.
- The pf-vdisplay driver's opt-in file log moves from world-writable
  C:\Users\Public to WUDFHost's own temp dir.
- GameStream pairing sessions are single-use (removed whatever the outcome).
- Uninstall also removes the pf_mouse driver-store entry (rider from the
  virtual-HID-mouse work).
- openapi.json regenerated (hardened-config-dir doc wording).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:20:15 +02:00
co-authored by Claude Fable 5
parent 4d89dcd3d7
commit 600693914f
21 changed files with 292 additions and 75 deletions
@@ -13,16 +13,7 @@ pub(crate) fn sanitize_device_name(name: &str, fp_hex: &str) -> String {
let cleaned: String = name
.chars()
.map(|c| if c == '\t' || c == '\n' { ' ' } else { c })
.filter(|&c| {
!c.is_control()
// Bidi/format controls that could spoof or reorder the displayed name.
&& !('\u{202A}'..='\u{202E}').contains(&c) // LRE..RLO/PDF
&& !('\u{2066}'..='\u{2069}').contains(&c) // LRI..PDI
&& c != '\u{200E}' // LRM
&& c != '\u{200F}' // RLM
&& c != '\u{061C}' // ALM
&& c != '\u{FEFF}' // BOM / zero-width no-break space
})
.filter(|&c| !c.is_control() && !is_spoofy_char(c))
.collect();
// Collapse internal whitespace runs, trim, cap at the wire limit.
let collapsed = cleaned.split_whitespace().collect::<Vec<_>>().join(" ");
@@ -42,6 +33,19 @@ pub(crate) fn sanitize_device_name(name: &str, fp_hex: &str) -> String {
}
}
/// A Unicode bidi/format control that could spoof or reorder a displayed name (an `RLO` making a
/// hostile device read like a trusted one). The canonical set — shared by every place that scrubs an
/// untrusted client name before display/storage (device names here, the stream marker) so the set
/// can't drift. Does NOT include C0/C1 controls; callers combine this with `char::is_control`.
pub(crate) fn is_spoofy_char(c: char) -> bool {
('\u{202A}'..='\u{202E}').contains(&c) // LRE..RLO/PDF
|| ('\u{2066}'..='\u{2069}').contains(&c) // LRI..PDI
|| c == '\u{200E}' // LRM
|| c == '\u{200F}' // RLM
|| c == '\u{061C}' // ALM
|| c == '\u{FEFF}' // BOM / zero-width no-break space
}
/// Max stored device-name length (matches the `Hello` wire cap, `quic::HELLO_NAME_MAX`).
const NAME_MAX: usize = 64;