flawopen.com/ssrf/Rust

● CWE-918 · Alta
Investigación · FlawOpen

Server-Side Request Forgery in Rust

How reqwest allows internal network queries in Rust, and how to validate resolved socket addresses before connecting.

💡 Explicación en Lenguaje Sencillo (ELI5)

Imagina enviar a un mensajero de oficina a recoger un paquete a una tienda pública, pero dándole la dirección de la caja fuerte del despacho del director. Como el mensajero tiene pase interno, entra al despacho, abre la caja y te entrega secretos de la empresa.

Conceptos Clave y Términos

Web Application Security
Componente de arquitectura central afectado por CWE-918.
CWE-918
Clasificación estándar Common Weakness Enumeration (CWE) para ssrf-rust.
Defense-in-Depth
Verificación de ingeniería multicapa y aislamiento de límites en tiempo de ejecución.

Flujo de Ataque Paso a Paso

Step 1

Ingestión de URL Remota No Confiable

La aplicación acepta una URL de usuario para descargar imágenes, webhooks o previsualizar documentos.

Step 2

Direccionamiento a Red Interna

El atacante suministra una URL apuntando a la dirección de metadatos de la nube o loopback (ej. http://169.254.169.254/latest/meta-data/).

Step 3

Salida de Socket Sin Restricciones

El cliente HTTP del servidor inicia la conexión desde la red privada interna sin validar si la IP pertenece a rangos reservados.

Step 4

Exfiltración de Credenciales de Nube

El servicio de metadados responde a la solicitud confiable entregando credenciales de IAM o tokens de seguridad.

Código Fuente: Vulnerable vs. Seguro

✕ IMPLEMENTACIÓN 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())
}
✓ PARCHE SEGURO Y ROBUSTO
// 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())
}

Lista de Verificación de Seguridad para Ingeniería

References