diff --git a/dstack/gateway/src/kv/mod.rs b/dstack/gateway/src/kv/mod.rs index 07dfcf228..7315bc1a6 100644 --- a/dstack/gateway/src/kv/mod.rs +++ b/dstack/gateway/src/kv/mod.rs @@ -682,6 +682,8 @@ impl KvStore { /// This stores the URL in KvStore (for address lookup) and also adds the node /// to the wavekv peer list (so SyncManager knows to sync with it). pub fn register_peer_url(&self, node_id: NodeId, url: &str) -> Result<()> { + validate_peer_url(url)?; + // Store URL in persistent KvStore self.persistent .write() @@ -1033,3 +1035,39 @@ impl KvStore { self.persistent.watch_prefix(keys::CERT_PREFIX) } } + +fn validate_peer_url(url: &str) -> Result<()> { + let parsed = reqwest::Url::parse(url).context("invalid peer URL")?; + anyhow::ensure!( + matches!(parsed.scheme(), "http" | "https"), + "peer URL scheme must be http or https" + ); + anyhow::ensure!(parsed.host_str().is_some(), "peer URL must include a host"); + anyhow::ensure!( + parsed.username().is_empty() && parsed.password().is_none(), + "peer URL must not contain credentials" + ); + Ok(()) +} + +#[cfg(test)] +mod peer_url_tests { + use super::validate_peer_url; + + #[test] + fn accepts_http_sync_urls() { + assert!(validate_peer_url("https://gateway.example:8011/sync").is_ok()); + assert!(validate_peer_url("http://127.0.0.1:8011").is_ok()); + } + + #[test] + fn rejects_malformed_or_unsafe_sync_urls() { + for url in [ + "not-a-sync-url", + "ftp://gateway.example/sync", + "https://user:secret@gateway.example/sync", + ] { + assert!(validate_peer_url(url).is_err(), "accepted {url}"); + } + } +}