use std::net::IpAddr;
use url::Url;
const ALLOW_PRIVATE_ENV: &str = "AUTHS_ALLOW_PRIVATE_REGISTRY";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub(crate) enum SsrfBlocked {
#[error("invalid registry URL: {0}")]
InvalidUrl(String),
#[error("registry URL scheme must be https, got '{0}'")]
InsecureScheme(String),
#[error("registry URL has no host")]
MissingHost,
#[error("refusing to dial a private or loopback registry host: {0}")]
BlockedHost(String),
}
pub(crate) fn guard_registry_url(url: &str) -> Result<(), SsrfBlocked> {
let allow_private = std::env::var_os(ALLOW_PRIVATE_ENV).is_some();
evaluate(url, allow_private)
}
fn evaluate(url: &str, allow_private: bool) -> Result<(), SsrfBlocked> {
let parsed = Url::parse(url).map_err(|e| SsrfBlocked::InvalidUrl(e.to_string()))?;
let host = parsed
.host_str()
.ok_or(SsrfBlocked::MissingHost)?
.to_string();
if allow_private {
return Ok(());
}
if parsed.scheme() != "https" {
return Err(SsrfBlocked::InsecureScheme(parsed.scheme().to_string()));
}
if is_blocked_host(&host) {
return Err(SsrfBlocked::BlockedHost(host));
}
Ok(())
}
pub(crate) fn is_blocked_host(host: &str) -> bool {
if let Ok(ip) = host.parse::<IpAddr>() {
return is_blocked_ip(ip);
}
matches!(host, "localhost" | "localhost.localdomain")
}
pub(crate) fn is_blocked_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
v4.is_loopback()
|| v4.is_private()
|| v4.is_link_local()
|| v4.is_unspecified()
|| v4.is_broadcast()
}
IpAddr::V6(v6) => {
let first = v6.segments()[0];
v6.is_loopback()
|| v6.is_unspecified()
|| (first & 0xfe00) == 0xfc00 || (first & 0xffc0) == 0xfe80 }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn evaluate_blocks_metadata_and_loopback_allows_public() {
assert_eq!(
evaluate("http://169.254.169.254/x", false),
Err(SsrfBlocked::InsecureScheme("http".to_string()))
);
assert_eq!(
evaluate("https://169.254.169.254/x", false),
Err(SsrfBlocked::BlockedHost("169.254.169.254".to_string()))
);
assert_eq!(
evaluate("http://127.0.0.1:8080", false),
Err(SsrfBlocked::InsecureScheme("http".to_string()))
);
assert_eq!(
evaluate("https://127.0.0.1:8080", false),
Err(SsrfBlocked::BlockedHost("127.0.0.1".to_string()))
);
assert_eq!(evaluate("https://registry.example.com", false), Ok(()));
}
#[test]
fn evaluate_allow_private_permits_loopback() {
assert_eq!(evaluate("http://127.0.0.1:8080", true), Ok(()));
assert_eq!(evaluate("http://169.254.169.254/x", true), Ok(()));
}
}