Skip to content

Commit f4f8687

Browse files
committed
fix(network): redact credentials from URL in allowlist error messages
URLs with user:pass@ in the authority are now redacted before inclusion in "not in allowlist" error messages. Closes #429
1 parent a802e63 commit f4f8687

File tree

1 file changed

+42
-1
lines changed

1 file changed

+42
-1
lines changed

crates/bashkit/src/network/allowlist.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@
1717
use std::collections::HashSet;
1818
use url::Url;
1919

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+
2035
/// Network allowlist configuration for controlling HTTP access.
2136
///
2237
/// URLs must match an entry in the allowlist to be accessed.
@@ -141,7 +156,7 @@ impl NetworkAllowlist {
141156
}
142157

143158
UrlMatch::Blocked {
144-
reason: format!("URL not in allowlist: {}", url),
159+
reason: format!("URL not in allowlist: {}", redact_url(url)),
145160
}
146161
}
147162

@@ -368,4 +383,30 @@ mod tests {
368383
let allow_all = NetworkAllowlist::allow_all();
369384
assert!(allow_all.is_enabled());
370385
}
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+
}
371412
}

0 commit comments

Comments
 (0)