flawopen.com/ssrf/Rust

● CWE-918 · Haute
Recherche · FlawOpen

Server-Side Request Forgery in Rust

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

💡 Explication en Langage Simple (ELI5)

Imaginez envoyer un coursier chercher un colis dans une boutique publique, mais en lui donnant l'adresse du coffre-fort du bureau de la direction. Comme le coursier possède un badge d'accès interne, il ouvre le coffre et vous remet les secrets de l'entreprise.

Concepts Clés et Termes

Web Application Security
Composant d'architecture clé affecté par CWE-918.
CWE-918
Classification standard Common Weakness Enumeration (CWE) pour ssrf-rust.
Defense-in-Depth
Vérification d'ingénierie multicouche et isolation des limites à l'exécution.

Déroulement de l'Attaque Étape par Étape

Step 1

Réception d'une URL Distante Non Fiable

L'application prend en charge une URL externe pour récupérer un avatar, un webhook ou convertir un document.

Step 2

Adressage de Réseau Interne

L'attaquant fournit une URL ciblant les métadonnées cloud (ex. http://169.254.169.254/latest/meta-data/) ou une IP privée de bouclage.

Step 3

Émission de Socket Non Filtrée

Le client HTTP du serveur émet une requête depuis le réseau privé sans résoudre au préalable l'adresse IP cible.

Step 4

Exfiltration des Identifiants Cloud

Le service de métadonnées interne fait confiance au serveur et renvoie les clés IAM temporaires ou les jetons d'accès.

Code Source : Vulnérable vs Sécurisé

✕ IMPLÉMENTATION VULNÉRABLE
// 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())
}
✓ PATCH SÉCURISÉ ET ROBUSTE
// 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())
}

Liste de Contrôle de Sécurité pour l'Ingénierie

References