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
This commit is contained in:
vicrodh committed 2026-05-24 07:59:03 -06:00
1 parent bf56eefd7a
commit 332c74bc39
2 files changed
+106 -3

No files matched your search

+10 -3
View File
@@ -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" {
+96
View File
@@ -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=<redacted>"`
///
/// 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("=<redacted>");
}
if first {
// Query delimiter was present but empty (`...?`).
out.push('?');
}
}
if had_fragment {
out.push_str("#<redacted>");
}
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=<redacted>"
);
}
#[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=<redacted>&k=<redacted>&co=<redacted>&hl=<redacted>"
);
}
#[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#<redacted>"
);
}
#[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=<redacted>#<redacted>"
);
}
}