From 332c74bc39e14f578389e39b2b390c929fea78ec Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 24 May 2026 07:59:03 -0600 Subject: [PATCH] chore(logging): redact query values and fragments in navigation logs Add log_sanitize::redact_url, which preserves a URL's scheme, host, path and query-parameter keys while masking every value and any fragment. Apply it to the OAuth WebView navigation log lines (main on_navigation, popup-window request, and popup on_navigation) so navigation traces stay useful for debugging the flow without carrying parameter values into log files or user-uploaded reports. Covered by unit tests for code/token params, base64-padded values, fragments, and query-less URLs. cc --- src-tauri/src/commands_v2/runtime.rs | 13 +++- src-tauri/src/log_sanitize.rs | 96 ++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands_v2/runtime.rs b/src-tauri/src/commands_v2/runtime.rs index 5fabe3393..bb574bed5 100644 --- a/src-tauri/src/commands_v2/runtime.rs +++ b/src-tauri/src/commands_v2/runtime.rs @@ -447,7 +447,10 @@ pub async fn v2_start_oauth_login( .inner_size(520.0, 720.0) .resizable(true) .on_navigation(move |url| { - log::info!("[OAuth] on_navigation: {}", url); + log::info!( + "[OAuth] on_navigation: {}", + crate::log_sanitize::redact_url(url.as_str()) + ); // Intercept redirect to play.qobuz.com/discover?code_autorisation=... if url.host_str() == Some("play.qobuz.com") { for (key, value) in url.query_pairs() { @@ -478,7 +481,7 @@ pub async fn v2_start_oauth_login( let label = format!("qobuz-oauth-popup-{}", popup_id); log::info!( "[OAuth] New popup window requested: {} (label={})", - url, + crate::log_sanitize::redact_url(url.as_str()), label ); @@ -497,7 +500,11 @@ pub async fn v2_start_oauth_login( .inner_size(520.0, 720.0) .resizable(true) .on_navigation(move |popup_url| { - log::info!("[OAuth] popup({}) on_navigation: {}", label_p, popup_url); + log::info!( + "[OAuth] popup({}) on_navigation: {}", + label_p, + crate::log_sanitize::redact_url(popup_url.as_str()) + ); if popup_url.host_str() == Some("play.qobuz.com") { for (key, value) in popup_url.query_pairs() { if key == "code_autorisation" || key == "code" { diff --git a/src-tauri/src/log_sanitize.rs b/src-tauri/src/log_sanitize.rs index 660f9e543..5a8e7511c 100644 --- a/src-tauri/src/log_sanitize.rs +++ b/src-tauri/src/log_sanitize.rs @@ -40,6 +40,60 @@ pub fn mask_id(id: impl Display) -> String { } } +/// Redact a URL for logging: keep the scheme/host/path and the query +/// parameter *keys*, but replace every query value and any fragment with a +/// placeholder. This keeps navigation logs useful for debugging the flow +/// (which page was reached, which params were present) without exposing the +/// values themselves — authorization codes, access tokens, encoded redirect +/// targets, captcha tokens, etc. +/// +/// `"https://play.qobuz.com/discover?code_autorisation=hg8mr52J"` +/// → `"https://play.qobuz.com/discover?code_autorisation="` +/// +/// Pure string manipulation so it never panics on malformed input: +/// `"about:blank"` and other query-less URLs are returned unchanged. +pub fn redact_url(raw: &str) -> String { + // Peel off the fragment first so a `#access_token=...` (implicit flow) + // can never survive in the output. + let (before_fragment, had_fragment) = match raw.split_once('#') { + Some((base, _)) => (base, true), + None => (raw, false), + }; + + let (base, query) = match before_fragment.split_once('?') { + Some((base, query)) => (base, Some(query)), + None => (before_fragment, None), + }; + + let mut out = String::from(base); + + if let Some(query) = query { + let mut first = true; + for pair in query.split('&') { + if pair.is_empty() { + continue; + } + out.push(if first { '?' } else { '&' }); + first = false; + // Key before the first '=' is kept; the value (and any further + // '=' inside it, e.g. base64 padding) is dropped. + let key = pair.split('=').next().unwrap_or(""); + out.push_str(key); + out.push_str("="); + } + if first { + // Query delimiter was present but empty (`...?`). + out.push('?'); + } + } + + if had_fragment { + out.push_str("#"); + } + + out +} + #[cfg(test)] mod tests { use super::*; @@ -81,4 +135,46 @@ mod tests { fn test_mask_id_five_digits() { assert_eq!(mask_id(10001), "1000****"); } + + #[test] + fn test_redact_url_oauth_code() { + assert_eq!( + redact_url("https://play.qobuz.com/discover?code_autorisation=hg8mr52J"), + "https://play.qobuz.com/discover?code_autorisation=" + ); + } + + #[test] + fn test_redact_url_multiple_params_and_base64_value() { + // Captcha-style URL: base64 values with '=' padding must be fully dropped. + assert_eq!( + redact_url("https://www.google.com/recaptcha/api2/anchor?ar=1&k=6LesW24a&co=aHR0cHM6Mw..&hl=en"), + "https://www.google.com/recaptcha/api2/anchor?ar=&k=&co=&hl=" + ); + } + + #[test] + fn test_redact_url_no_query() { + assert_eq!(redact_url("about:blank"), "about:blank"); + assert_eq!( + redact_url("https://www.qobuz.com/signin/oauth"), + "https://www.qobuz.com/signin/oauth" + ); + } + + #[test] + fn test_redact_url_fragment() { + assert_eq!( + redact_url("https://example.com/cb#access_token=abc123&id=9"), + "https://example.com/cb#" + ); + } + + #[test] + fn test_redact_url_query_and_fragment() { + assert_eq!( + redact_url("https://example.com/cb?code=xyz#access_token=abc"), + "https://example.com/cb?code=#" + ); + } }