Skip to main content

cdk_common/
redact.rs

1//! Helpers for formatting potentially sensitive values safely.
2
3use url::Url;
4
5const INVALID_URL_FOR_LOGS: &str = "[INVALID URL]";
6
7/// Return a URL suitable for logs and `Debug` output by removing userinfo,
8/// query parameters, and fragments.
9///
10/// Malformed URLs and URL forms whose userinfo cannot be removed are replaced
11/// entirely so the original value cannot leak through a fallback path.
12pub fn url_for_logs(value: &str) -> String {
13    let Ok(mut url) = Url::parse(value) else {
14        return INVALID_URL_FOR_LOGS.to_owned();
15    };
16
17    if url.set_password(None).is_err() || url.set_username("").is_err() {
18        return INVALID_URL_FOR_LOGS.to_owned();
19    }
20
21    url.set_query(None);
22    url.set_fragment(None);
23
24    url.to_string()
25}
26
27#[cfg(test)]
28mod tests {
29    use super::{url_for_logs, INVALID_URL_FOR_LOGS};
30
31    #[test]
32    fn removes_url_credentials() {
33        let url = "https://alice:s3cr3t@example.com:8443/api?network=main#tip";
34
35        let logged_url = url_for_logs(url);
36
37        assert_eq!(logged_url, "https://example.com:8443/api");
38        assert!(!logged_url.contains("alice"));
39        assert!(!logged_url.contains("s3cr3t"));
40    }
41
42    #[test]
43    fn removes_credentials_from_custom_scheme() {
44        let url = "ssl://alice:s3cr3t@example.com:50002";
45
46        assert_eq!(url_for_logs(url), "ssl://example.com:50002");
47    }
48
49    #[test]
50    fn preserves_ipv6_port_and_path() {
51        let url = "https://user:pass@[2001:db8::1]:3002/api";
52
53        assert_eq!(url_for_logs(url), "https://[2001:db8::1]:3002/api");
54    }
55
56    #[test]
57    fn preserves_url_without_credentials() {
58        let url = "https://example.com/api";
59
60        assert_eq!(url_for_logs(url), url);
61    }
62
63    #[test]
64    fn removes_query_parameters_and_fragments() {
65        let url = "https://example.com/api?token=query-secret#fragment-secret";
66
67        let logged_url = url_for_logs(url);
68
69        assert_eq!(logged_url, "https://example.com/api");
70        assert!(!logged_url.contains("query-secret"));
71        assert!(!logged_url.contains("fragment-secret"));
72    }
73
74    #[test]
75    fn malformed_url_is_not_logged() {
76        let url = "not a URL with user:secret@example.com";
77
78        let logged_url = url_for_logs(url);
79
80        assert_eq!(logged_url, INVALID_URL_FOR_LOGS);
81        assert!(!logged_url.contains("secret"));
82    }
83}