flawopen.com/Server-Side Request Forgery/Swift

Server-Side Request Forgery in Swift

High Severity CWE-918
ELI5

You give a robot courier an order slip. The courier is instructed to only visit public storefronts. If you don't inspect the address, the courier walks into your private company basement.

Key terms on this page
SocketAddr
An internet socket address (IPv4 or IPv6 with port) in Rust's standard library.
reqwest::redirect::Policy
Controls redirect handling in Rust's reqwest client.

What's happening

Calling reqwest::get(&url).await in Rust connects to any routable address. Attackers supply internal endpoints to probe internal microservices.

Real-world impact

Vulnerabilities in modern Rust backend microservices that fetch remote avatars, webhooks, or open-graph cards have led to internal infrastructure port scanning.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
// 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())
}
FIXED
// 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())
}

Why the fix works

The code uses to_socket_addrs() to evaluate all destination IPs. Checking !ipv4.is_private() and !ipv4.is_link_local() blocks RFC 1918 and IMDS endpoints. Policy::none() blocks redirect exploitation.

Gotchas

IPv6 unique local addresses

Ensure IPv6 addresses in fc00::/7 (ULA) are checked and blocked in production.

Common misconceptions

"Type safety in Rust prevents SSRF"

Type safety prevents memory corruption, but logical network vulnerabilities like SSRF are independent of memory safety.

How to check if you're affected

cargo clippy cargo audit

Prevention checklist

FAQ

Does reqwest follow redirects by default?

Yes, reqwest follows up to 10 redirects by default. You must explicitly configure Policy::none() to prevent SSRF redirect bypasses.

References