feat: add log sanitization for sensitive IDs and UUIDs

Add log_sanitize module with mask_uuid() and mask_id() functions to
prevent security scanners from flagging user IDs and session UUIDs
in log output.

- mask_uuid: 123e4567-e89b-12d3-a456-426614174000 -> 123e4567-****-****-****-****14174000
- mask_id: 12345678 -> 1234****
- Applied to offline_user_id in session_lifecycle (CodeQL alert #50)
- Applied to session_uuid and command uuid in qconnect_service

cc
This commit is contained in:
victorrhgap committed 2026-03-31 12:05:24 -06:00
1 parent 8b38316a85
commit 2900fb882c
4 files changed
+88 -3

No files matched your search

+1
View File
@@ -31,6 +31,7 @@ pub mod image_cache;
pub mod lastfm;
pub mod library;
pub mod listenbrainz;
pub mod log_sanitize;
pub mod logging;
pub mod lyrics;
pub mod media_controls;
+84
View File
@@ -0,0 +1,84 @@
//! Sanitization helpers for sensitive values in log output.
//!
//! Masks UUIDs and numeric IDs so logs remain useful for debugging
//! without exposing full identifiers to security scanners or uploaded
//! log files.
use std::fmt::Display;
/// Mask a UUID string, preserving first and last 8 characters.
///
/// `"123e4567-e89b-12d3-a456-426614174000"` → `"123e4567-****-****-****-****14174000"`
///
/// Non-UUID strings are returned with middle characters masked.
pub fn mask_uuid(uuid: &str) -> String {
// Standard UUID: 8-4-4-4-12 = 36 chars
if uuid.len() == 36 && uuid.chars().filter(|c| *c == '-').count() == 4 {
let first = &uuid[..8];
let last = &uuid[28..];
format!("{first}-****-****-****-****{last}")
} else if uuid.len() > 8 {
let quarter = uuid.len() / 4;
let first = &uuid[..quarter];
let last = &uuid[uuid.len() - quarter..];
format!("{first}****{last}")
} else {
"****".to_string()
}
}
/// Mask a numeric or string ID, preserving at most the first 4 characters.
///
/// `12345678` → `"1234****"`
/// `42` → `"****"`
pub fn mask_id(id: impl Display) -> String {
let s = id.to_string();
if s.len() > 4 {
format!("{}****", &s[..4])
} else {
"****".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mask_uuid_standard() {
assert_eq!(
mask_uuid("123e4567-e89b-12d3-a456-426614174000"),
"123e4567-****-****-****-****14174000"
);
}
#[test]
fn test_mask_uuid_short() {
assert_eq!(mask_uuid("abcd"), "****");
}
#[test]
fn test_mask_uuid_non_standard() {
assert_eq!(mask_uuid("abcdef1234567890"), "abcd****7890");
}
#[test]
fn test_mask_id_long() {
assert_eq!(mask_id(12345678), "1234****");
}
#[test]
fn test_mask_id_short() {
assert_eq!(mask_id(42), "****");
}
#[test]
fn test_mask_id_zero() {
assert_eq!(mask_id(0), "****");
}
#[test]
fn test_mask_id_five_digits() {
assert_eq!(mask_id(10001), "1000****");
}
}
+2 -2
View File
@@ -3926,7 +3926,7 @@ async fn deferred_renderer_join(
log::error!("[QConnect] Deferred renderer max quality report failed: {err}");
}
log::info!("[QConnect] Deferred renderer join complete for session {session_uuid}");
log::info!("[QConnect] Deferred renderer join complete for session {}", crate::log_sanitize::mask_uuid(&session_uuid));
// Re-request session state so the server sends an updated renderer list
// (including ourselves). Without this, the frontend may not see QBZ as a
@@ -4151,7 +4151,7 @@ pub async fn v2_qconnect_send_command_with_admission(
.await
{
Ok(uuid) => {
log::info!("[QConnect] send_command_with_admission: sent uuid={uuid}");
log::info!("[QConnect] send_command_with_admission: sent uuid={}", crate::log_sanitize::mask_uuid(&uuid));
Ok(uuid)
}
Err(err) => {
+1 -1
View File
@@ -346,7 +346,7 @@ pub async fn activate_offline_session(app: &tauri::AppHandle) -> Result<(), Stri
let offline_user_id = UserDataPaths::load_last_user_id().unwrap_or(0);
log::info!(
"[SessionLifecycle] Activating offline session (user_id={}{})",
offline_user_id,
crate::log_sanitize::mask_id(offline_user_id),
if offline_user_id == 0 { " — no previous session" } else { "" }
);