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:
4 files changed
+88
-3
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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****");
|
||||
}
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 { "" }
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user