Skip to main content

pharos_native/runtime/
outbound_url.rs

1//! Shared URL admission for public source configuration and evidence links.
2
3/// Parses canonical HTTPS URLs without embedded credentials or ambiguous control characters.
4/// Actual outbound connections must additionally enforce the transport's resolved-address policy.
5pub(super) fn https_url(value: &str) -> Option<url::Url> {
6    web_url(value).filter(|url| url.scheme() == "https")
7}
8
9/// Admits ordinary public website links while excluding executable schemes and credentials.
10/// This does not authorize a fetch; discovery continues to require the HTTPS transport policy.
11pub(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    /// Keeps URL normalization and rejection identical for forms and the discovery worker.
41    #[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}