flawopen.com/ssrf/Swift

● CWE-918 · 高
セキュリティ研究 · FlawOpen

脆弱性の解説:Server-Side Request Forgery in Swift

How URLSession allows internal IP queries in Swift/Vapor, and how to enforce IP boundaries.

💡 わかりやすい解説 (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-swift.
Defense-in-Depth
主要概念 (Defense-in-Depth):Multi-layered engineering verification and runtime boundary isolation.

ステップ・バイ・ステップの攻撃フロー

Step 1

外部URLパラメータの受信

アプリケーションがユーザーから指定されたURLを受け取り、画像の取得やWebhook配信を実行します。

Step 2

内部ネットワークアドレスの指定

攻撃者がクラウドメタデータサービス(例: http://169.254.169.254/latest/meta-data/)や内部ループバックURLを指定します。

Step 3

保護領域からの制限なきソケット送信

サーバーのHTTPクライアントがプライベートVPC内からリクエストを発行し、内部IPチェックを行いません。

Step 4

クラウド認証情報およびトークンの窃取

メタデータサービスがリクエストを信頼し、一時的なIAM認証情報や内部APIデータを攻撃者に返却します。

ソースコード比較:脆弱 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