flawopen.com/ssrf/Rust
How reqwest allows internal network queries in Rust, and how to validate resolved socket addresses before connecting.
Bayangkan mengirim kurir kantor untuk mengambil paket di toko umum, tetapi memberinya alamat brankas terkunci di ruang manajer. Karena kurir memiliki lencana akses internal, dia membuka brankas dan menyerahkan rahasia perusahaan kepada Anda.
Web Application SecurityCWE-918.CWE-918Defense-in-DepthAplikasi menerima parameter URL dari pengguna untuk mengunduh avatar, webhook, atau pratinjau dokumen.
Penyerang memasukkan alamat loopback atau layanan metadata cloud (misal: http://169.254.169.254/latest/meta-data/).
Klien HTTP server mengirim permintaan dari dalam VPC privat tanpa memeriksa rentang subnet privat.
Layanan metadata internal mempercayai permintaan dan mengembalikan token kredensial IAM sementara ke penyerang.
// 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.