pharos_native/runtime/
outbound_url.rs1pub(super) fn https_url(value: &str) -> Option<url::Url> {
6 web_url(value).filter(|url| url.scheme() == "https")
7}
8
9pub(super) fn web_url(value: &str) -> Option<url::Url> {
12 if value.len() > 2048
13 || value.chars().any(|ch| {
14 ch.is_control() || ch.is_whitespace() || matches!(ch, '<' | '>' | '"' | '\'' | '\\')
15 })
16 {
17 return None;
18 }
19 let parsed = url::Url::parse(value).ok()?;
20 if !matches!(parsed.scheme(), "http" | "https")
21 || parsed.host_str().is_none()
22 || !parsed.username().is_empty()
23 || parsed.password().is_some()
24 {
25 return None;
26 }
27 if parsed.query_pairs().any(|(key, _)| {
28 matches!(
29 key.to_ascii_lowercase().as_str(),
30 "access_token" | "api_key" | "client_secret" | "password" | "authorization"
31 )
32 }) {
33 return None;
34 }
35 Some(parsed)
36}
37
38#[cfg(test)]
39mod tests {
40 #[test]
42 fn canonical_https_admission_rejects_credentials_and_unsafe_schemes() {
43 assert!(super::web_url("http://example.org/profile").is_some());
44 assert!(super::https_url("http://example.org/profile").is_none());
45 assert_eq!(
46 super::https_url("https://EXAMPLE.org/public/../events")
47 .unwrap()
48 .as_str(),
49 "https://example.org/events"
50 );
51 for value in [
52 "javascript:alert(1)",
53 "https://user:secret@example.org/",
54 "https://example.org/?access_token=secret",
55 "https://example.org/\\private",
56 ] {
57 assert!(super::https_url(value).is_none());
58 assert!(super::web_url(value).is_none());
59 }
60 }
61}