flawopen.com/ssrf/Rust
How reqwest allows internal network queries in Rust, and how to validate resolved socket addresses before connecting.
Imagine enviar um assistente de escritório para buscar um pacote em uma loja pública, mas fornecendo o endereço do cofre trancado na sala da gerência. Como o assistente tem um crachá interno, ele abre o cofre e entrega segredos da empresa.
Web Application SecurityCWE-918.CWE-918Defense-in-DepthO aplicativo recebe uma URL de usuário para baixar webhooks, buscar avatares ou gerar relatórios PDF.
O invasor fornece um endereço local ou do serviço de metadados de nuvem (ex: http://169.254.169.254/latest/meta-data/).
O cliente HTTP do servidor inicia a conexão de dentro da VPC privada sem validar o IP resolvido ou sub-rede.
O serviço interno de metadados confia no IP do servidor e retorna credenciais temporárias de IAM ou tokens de acesso.
// VULNERABLE: Unchecked reqwest::get
use reqwest;
async fn fetch_remote_resource(user_url: &str) -> Result<Vec<u8>, reqwest::Error> {
// Attacker passes: http://169.254.169.254/latest/meta-data/
let resp = reqwest::get(user_url).await?;
let bytes = resp.bytes().await?;
Ok(bytes.to_vec())
}
// HARDENED: Resolve DNS, assert non-private IP, and disable redirects
use std::net::{IpAddr, ToSocketAddrs};
use reqwest::redirect::Policy;
use url::Url;
fn is_safe_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(ipv4) => {
!ipv4.is_loopback() &&
!ipv4.is_private() &&
!ipv4.is_link_local() &&
!ipv4.is_unspecified()
}
IpAddr::V6(ipv6) => {
!ipv6.is_loopback() &&
!ipv6.is_unspecified()
}
}
}
async fn fetch_remote_resource(user_url: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let parsed_url = Url::parse(user_url)?;
if parsed_url.scheme() != "http" && parsed_url.scheme() != "https" {
return Err("Invalid URL scheme".into());
}
let host = parsed_url.host_str().ok_or("Missing host")?;
let port = parsed_url.port_or_known_default().unwrap_or(80);
// 1. Resolve host and inspect all IP addresses
let socket_addrs: Vec<_> = format!("{}:{}", host, port).to_socket_addrs()?.collect();
for sa in &socket_addrs {
if !is_safe_ip(sa.ip()) {
return Err("Access to private/internal IP rejected".into());
}
}
// 2. Build client with redirects disabled
let client = reqwest::Client::builder()
.redirect(Policy::none())
.timeout(std::time::Duration::from_secs(3))
.build()?;
let resp = client.get(parsed_url).send().await?;
let bytes = resp.bytes().await?;
Ok(bytes.to_vec())
}
ip.is_private() && !ip.is_link_local().none() on reqwest Client.