flawopen.com/ssrf/Rust
How reqwest allows internal network queries in Rust, and how to validate resolved socket addresses before connecting.
Stellen Sie sich vor, Sie schicken einen Boten, um ein Paket in einem öffentlichen Geschäft abzuholen, geben ihm aber die Adresse des Tresors im Büro des Geschäftsführers. Da der Bote einen internen Ausweis besitzt, öffnet er den Tresor und händigt Firmengeheimnisse aus.
Web Application SecurityCWE-918 betroffen ist.CWE-918Defense-in-DepthDie Anwendung akzeptiert externe URL-Parameter für Webhooks, Avatare oder PDF-Voransichten.
Der Angreifer übergibt eine Ziel-URL auf Cloud-Metadaten oder interne Loopback-Adressen (z. B. http://169.254.169.254/latest/meta-data/).
Der HTTP-Client des Servers sendet die Anfrage aus der geschützten VPC, ohne die Ziel-IP gegen private Subnetzbereiche abzugleichen.
Der interne Metadatendienst vertraut der Server-Anfrage und liefert temporäre IAM-Schlüssel und Tokens zurück.
// 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.