flawopen.com/ssrf/Rust

● CWE-918 · Высокий
Исследования · FlawOpen

Анализ уязвимости: Server-Side Request Forgery in Rust

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

💡 Простыми словами (ELI5)

Представьте, что вы отправляете курьера забрать посылку в общественном магазине, но указываете адрес запертого сейфа в кабинете директора. Поскольку у курьера есть служебный пропуск, он заходит в кабинет, открывает сейф и приносит секреты компании.

Ключевые понятия и термины

Web Application Security
Технический термин (Web Application Security): Core architecture component affected by CWE-918.
CWE-918
Технический термин (CWE-918): Standard Common Weakness Enumeration classification for ssrf-rust.
Defense-in-Depth
Технический термин (Defense-in-Depth): Multi-layered engineering verification and runtime boundary isolation.

Пошаговый сценарий атаки

Step 1

Прием удаленного URL от пользователя

Сервис принимает внешний URL для загрузки аватарок, вебхуков или генерации документов.

Step 2

Указание адреса внутренней сети

Атакующий передает адрес сервиса метаданных облака (http://169.254.169.254/latest/meta-data/) или локальный адрес loopback.

Step 3

Неконтролируемое исходящее соединение

Сервер отправляет HTTP-запрос изнутри защищенного периметра без проверки принадлежности IP к приватным подсетям.

Step 4

Кража облачных учетных данных

Сервис метаданных доверяет запросу с локального интерфейса и отдает временные IAM-токены и секреты кластера.

Исходный код: Уязвимый vs Защищённый вариант

✕ УЯЗВИМАЯ РЕАЛИЗАЦИЯ
// 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())
}

Чек-лист по защите системы для инженеров

References