use anyhow::{anyhow, Result};
use colored::*;
use std::net::IpAddr;
use std::path::Path;
use url::Url;
pub fn validate_url(url: &str) -> Result<()> {
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err(anyhow!("Only HTTP and HTTPS URLs are allowed."));
}
let parsed_url = Url::parse(url).map_err(|e| anyhow!("Invalid URL format: {}", e))?;
let host = parsed_url
.host_str()
.ok_or_else(|| anyhow!("URL must have a host"))?;
let host_lower = host.to_lowercase();
if host_lower == "localhost"
|| host_lower == "localhost.localdomain"
|| host_lower.starts_with("localhost:")
|| host_lower.ends_with(".local")
|| host_lower.ends_with(".localhost")
{
return Err(anyhow!(
"Access to localhost is not allowed. Use actual hostnames or IPs."
));
}
if let Ok(ip) = host.parse::<IpAddr>() {
match ip {
IpAddr::V4(ipv4) => {
if ipv4.is_loopback() {
return Err(anyhow!("Access to loopback addresses is not allowed."));
}
if ipv4.is_private() {
return Err(anyhow!(
"Access to private IP ranges is not allowed for security reasons."
));
}
if ipv4.is_link_local() {
return Err(anyhow!("Access to link-local addresses is not allowed."));
}
if ipv4.is_broadcast() {
return Err(anyhow!("Access to broadcast addresses is not allowed."));
}
if ipv4.is_unspecified() {
return Err(anyhow!("Access to unspecified addresses is not allowed."));
}
if ipv4.is_multicast() {
return Err(anyhow!("Access to multicast addresses is not allowed."));
}
if ipv4.octets() == [169, 254, 169, 254] {
return Err(anyhow!(
"Access to cloud metadata endpoints is not allowed."
));
}
}
IpAddr::V6(ipv6) => {
if ipv6.is_loopback() {
return Err(anyhow!("Access to loopback addresses is not allowed."));
}
if ipv6.is_unspecified() {
return Err(anyhow!("Access to unspecified addresses is not allowed."));
}
if ipv6.is_multicast() {
return Err(anyhow!("Access to multicast addresses is not allowed."));
}
if let Some(ipv4) = ipv6.to_ipv4_mapped() {
if ipv4.is_loopback() || ipv4.is_private() || ipv4.is_link_local() {
return Err(anyhow!(
"Access to private/local addresses via IPv6 mapping is not allowed."
));
}
}
}
}
}
let blocked_hosts = [
"metadata.google.internal",
"metadata.google",
"metadata.goog",
"metadata.amazon",
"metadata.azure",
"instance-data",
"instance.metadata",
];
for blocked in &blocked_hosts {
if host_lower.contains(blocked) {
return Err(anyhow!(
"Access to cloud metadata endpoints is not allowed."
));
}
}
match parsed_url.scheme() {
"http" | "https" => Ok(()),
_ => Err(anyhow!("Only HTTP and HTTPS protocols are allowed.")),
}
}
pub fn enforce_https(url: &str, allow_insecure: bool) -> Result<()> {
validate_url(url)?;
if url.starts_with("http://") && !allow_insecure {
let url_lower = url.to_lowercase();
if url_lower.starts_with("http://localhost")
|| url_lower.starts_with("http://127.0.0.1")
|| url_lower.starts_with("http://0.0.0.0")
{
return Err(anyhow!(
"HTTP is not allowed. Use HTTPS or pass --allow-insecure flag (not recommended)."
));
}
return Err(anyhow!(
"Insecure HTTP connection blocked: {}\n\
\n\
{} {}\n\
\n\
HTTP connections are vulnerable to:\n\
• Man-in-the-middle attacks\n\
• Credential theft\n\
• Data tampering\n\
\n\
To use HTTP anyway (NOT RECOMMENDED):\n\
Add --allow-insecure flag to your command\n\
\n\
Better solution: Use HTTPS URLs",
url,
"⚠️".red().bold(),
"SECURITY WARNING".red().bold()
));
}
if url.starts_with("http://") && allow_insecure {
eprintln!("\n{}", "━".repeat(60).red());
eprintln!(
"{} {} {}",
"⚠️".red().bold(),
"INSECURE CONNECTION WARNING".red().bold(),
"⚠️".red().bold()
);
eprintln!("{}", "━".repeat(60).red());
eprintln!("{} Using insecure HTTP connection to:", "⚠️".yellow());
eprintln!(" {}", url.yellow());
eprintln!();
eprintln!(
"{}",
"This connection is NOT encrypted and vulnerable to:".red()
);
eprintln!(" • {} Man-in-the-middle attacks", "❌".red());
eprintln!(" • {} Credential and API key theft", "❌".red());
eprintln!(" • {} Data tampering and injection", "❌".red());
eprintln!(" • {} Request/response interception", "❌".red());
eprintln!();
eprintln!(
"{} {}",
"👉".cyan(),
"Recommendation: Use HTTPS instead".cyan().bold()
);
eprintln!("{}", "━".repeat(60).red());
eprintln!();
}
Ok(())
}
pub fn validate_file_path(path: &Path) -> Result<()> {
let path_str = path.to_string_lossy();
if path_str.contains("..") {
return Err(anyhow!("Path traversal is not allowed"));
}
let blocked_paths = [
"/etc/passwd",
"/etc/shadow",
"/etc/sudoers",
"/.ssh/",
"/root/",
"/proc/",
"/sys/",
"/.aws/",
"/.kube/",
"/.docker/",
"/.git/credentials",
"/.netrc",
"/.npmrc",
];
for blocked in &blocked_paths {
if path_str.contains(blocked) {
return Err(anyhow!("Access to {} is not allowed", blocked));
}
}
if cfg!(windows) {
let blocked_windows = [
"C:\\Windows\\System32",
"C:\\Windows\\System",
"C:\\Program Files",
];
for blocked in &blocked_windows {
if path_str.contains(blocked) {
return Err(anyhow!("Access to {} is not allowed", blocked));
}
}
}
Ok(())
}
pub fn validate_output_path(path: &Path) -> Result<()> {
validate_file_path(path)?;
let path_str = path.to_string_lossy();
let blocked_write_paths = [
"/usr/",
"/bin/",
"/sbin/",
"/lib/",
"/lib64/",
"/etc/",
"/boot/",
"/dev/",
"/opt/",
"/var/lib/",
"/var/run/",
];
for blocked in &blocked_write_paths {
if path_str.starts_with(blocked) {
return Err(anyhow!("Cannot write to system directory: {}", blocked));
}
}
if cfg!(windows) {
let blocked_windows = [
"C:\\Windows",
"C:\\Program Files",
"C:\\ProgramData",
"C:\\System",
];
for blocked in &blocked_windows {
if path_str.starts_with(blocked) {
return Err(anyhow!("Cannot write to system directory: {}", blocked));
}
}
}
Ok(())
}
pub fn validate_delete_path(path: &Path) -> Result<()> {
validate_output_path(path)?;
let path_str = path.to_string_lossy();
let critical_paths = ["/", "/home", "/Users", "~", ".", ".."];
for critical in &critical_paths {
if path_str == *critical || path_str.ends_with(critical) {
return Err(anyhow!("Cannot delete critical directory: {}", critical));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_url_validation() {
assert!(validate_url("http://localhost/api").is_err());
assert!(validate_url("http://127.0.0.1/api").is_err());
assert!(validate_url("http://192.168.1.1/api").is_err());
assert!(validate_url("http://10.0.0.1/api").is_err());
assert!(validate_url("http://172.16.0.1/api").is_err());
assert!(validate_url("http://169.254.169.254/metadata").is_err());
assert!(validate_url("file:///etc/passwd").is_err());
assert!(validate_url("https://api.example.com").is_ok());
assert!(validate_url("http://8.8.8.8/api").is_ok());
}
#[test]
fn test_file_path_validation() {
assert!(validate_file_path(Path::new("/etc/passwd")).is_err());
assert!(validate_file_path(Path::new("../../../etc/passwd")).is_err());
assert!(validate_file_path(Path::new("/home/user/.ssh/id_rsa")).is_err());
assert!(validate_file_path(Path::new("/home/user/project/api.yaml")).is_ok());
assert!(validate_file_path(Path::new("./specs/api.yaml")).is_ok());
}
#[test]
fn test_output_path_validation() {
assert!(validate_output_path(Path::new("/etc/test.yaml")).is_err());
assert!(validate_output_path(Path::new("/usr/bin/test")).is_err());
assert!(validate_output_path(Path::new("/tmp/test.yaml")).is_ok());
assert!(validate_output_path(Path::new("./output/sdk/")).is_ok());
}
}