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
+14 -4
View File
@@ -306,8 +306,8 @@ pub(crate) async fn list_compositors() -> Json<Vec<AvailableCompositor>> {
)]
pub(crate) async fn get_status(State(st): State<Arc<MgmtState>>) -> Json<RuntimeStatus> {
// GameStream plane (set by RTSP/nvhttp on the compat path).
let gs_launch = *st.app.launch.lock().unwrap();
let gs_stream = *st.app.stream.lock().unwrap();
let gs_launch = *st.app.launch.lock().unwrap_or_else(|e| e.into_inner());
let gs_stream = *st.app.stream.lock().unwrap_or_else(|e| e.into_inner());
let gs_video = st.app.streaming.load(Ordering::SeqCst);
let gs_audio = st.app.audio_streaming.load(Ordering::SeqCst);
// Native punktfunk/1 plane (published by the native video loop; the default plane). See
@@ -362,7 +362,12 @@ pub(crate) async fn get_status(State(st): State<Arc<MgmtState>>) -> Json<Runtime
video_streaming: gs_video || !native.is_empty(),
audio_streaming: gs_audio || !native.is_empty(),
pin_pending: st.app.pairing.pin.awaiting_pin(),
paired_clients: st.app.paired.lock().unwrap().len() as u32,
paired_clients: st
.app
.paired
.lock()
.unwrap_or_else(|e| e.into_inner())
.len() as u32,
active_sessions: native.len() as u32 + u32::from(gs_video),
session,
stream,
@@ -417,7 +422,12 @@ pub(crate) async fn get_local_summary(State(st): State<Arc<MgmtState>>) -> Json<
video_streaming: st.app.streaming.load(Ordering::SeqCst),
audio_streaming: st.app.audio_streaming.load(Ordering::SeqCst),
session,
paired_clients: st.app.paired.lock().unwrap().len() as u32,
paired_clients: st
.app
.paired
.lock()
.unwrap_or_else(|e| e.into_inner())
.len() as u32,
native_paired_clients,
pin_pending: st.app.pairing.pin.awaiting_pin(),
pending_approvals,