|
17 | 17 | use std::collections::HashSet; |
18 | 18 | use url::Url; |
19 | 19 |
|
| 20 | +/// Redact credentials from a URL for safe inclusion in error messages. |
| 21 | +/// Replaces `user:pass@` in the authority with `***@`. |
| 22 | +fn redact_url(url: &str) -> String { |
| 23 | + match Url::parse(url) { |
| 24 | + Ok(mut parsed) => { |
| 25 | + if !parsed.username().is_empty() || parsed.password().is_some() { |
| 26 | + let _ = parsed.set_username("***"); |
| 27 | + let _ = parsed.set_password(None); |
| 28 | + } |
| 29 | + parsed.to_string() |
| 30 | + } |
| 31 | + Err(_) => "[invalid URL]".to_string(), |
| 32 | + } |
| 33 | +} |
| 34 | + |
20 | 35 | /// Network allowlist configuration for controlling HTTP access. |
21 | 36 | /// |
22 | 37 | /// URLs must match an entry in the allowlist to be accessed. |
@@ -141,7 +156,7 @@ impl NetworkAllowlist { |
141 | 156 | } |
142 | 157 |
|
143 | 158 | UrlMatch::Blocked { |
144 | | - reason: format!("URL not in allowlist: {}", url), |
| 159 | + reason: format!("URL not in allowlist: {}", redact_url(url)), |
145 | 160 | } |
146 | 161 | } |
147 | 162 |
|
@@ -368,4 +383,30 @@ mod tests { |
368 | 383 | let allow_all = NetworkAllowlist::allow_all(); |
369 | 384 | assert!(allow_all.is_enabled()); |
370 | 385 | } |
| 386 | + |
| 387 | + #[test] |
| 388 | + fn test_redact_url_strips_credentials() { |
| 389 | + let redacted = redact_url("https://user:secret@example.com/path"); |
| 390 | + assert!(!redacted.contains("secret"), "password leaked: {}", redacted); |
| 391 | + assert!(!redacted.contains("user"), "username leaked: {}", redacted); |
| 392 | + assert!(redacted.contains("example.com/path")); |
| 393 | + } |
| 394 | + |
| 395 | + #[test] |
| 396 | + fn test_redact_url_preserves_clean_url() { |
| 397 | + let clean = "https://example.com/path?q=1"; |
| 398 | + assert_eq!(redact_url(clean), clean); |
| 399 | + } |
| 400 | + |
| 401 | + #[test] |
| 402 | + fn test_blocked_message_no_credentials() { |
| 403 | + let allowlist = NetworkAllowlist::new().allow("https://allowed.com"); |
| 404 | + let result = allowlist.check("https://user:pass@blocked.com/api"); |
| 405 | + match result { |
| 406 | + UrlMatch::Blocked { reason } => { |
| 407 | + assert!(!reason.contains("pass"), "credentials leaked: {}", reason); |
| 408 | + } |
| 409 | + _ => panic!("expected Blocked"), |
| 410 | + } |
| 411 | + } |
371 | 412 | } |
0 commit comments