PHP'de SSRF'ye karşı URL doğrulama: parse_url yerine yeni URI eklentisi ve IP kontrolü

Sabit admin açtı, 3 gün önce 0 yanıt 10 görüntülenme
admin Admin
  • 0 mesaj
  • 1K puan
  • Katılım 12.2025
  • Konu sahibi

OWASP Top 10:2025'te SSRF artık "Erişim kontrolü hataları" (A01) altında. Uygulamanız kullanıcının verdiği bir adrese istek atıyorsa (profil resmini URL'den indirme, webhook, bağlantı önizleme) saldırgan bu özelliği iç ağa, bulut meta veri servisine (169.254.169.254) ya da localhost'taki yönetim panellerine erişmek için kullanabilir.

parse_url neden yetmiyor?

parse_url() standarda tam uymadığı için, cURL'ün ve tarayıcıların farklı yorumladığı adreslerde yanlış ana bilgisayar adı döndürebiliyor. Doğrulayıcı bir adresi kontrol edip, istemci başka bir adrese gidince güvenlik kontrolü atlanmış oluyor. PHP 8.5 ile gelen URI eklentisi, RFC 3986 ve WHATWG standartlarına göre ayrıştırıyor.

Güvenli bir akış

  1. Adresi standart bir ayrıştırıcıyla ayrıştırın, yalnızca http/https kabul edin.
  2. Ana bilgisayar adını DNS ile çözün ve dönen tüm IP'leri kontrol edin: özel, döngü (loopback), yerel bağlantı ve ayrılmış aralıklar reddedilsin.
  3. İsteği, kontrol ettiğiniz IP'ye sabitleyerek yapın (DNS rebinding'e karşı).
  4. Yönlendirmeleri kapatın ya da her yönlendirmede aynı kontrolü tekrarlayın.
use Uri\WhatWg\Url;

function guvenliGetir(string $girdi): string
{
    try {
        $url = new Url($girdi);              // geçersizse istisna fırlatır
    } catch (\Throwable $e) {
        throw new InvalidArgumentException('Geçersiz adres');
    }
    if (!in_array($url->getScheme(), ['http', 'https'], true)) {
        throw new InvalidArgumentException('Yalnızca http/https');
    }
    $host = $url->getAsciiHost();
    $port = $url->getPort() ?? ($url->getScheme() === 'https' ? 443 : 80);

    $ips = array_merge(
        array_column(dns_get_record($host, DNS_A) ?: [], 'ip'),
        array_column(dns_get_record($host, DNS_AAAA) ?: [], 'ipv6')
    );
    if (!$ips) throw new RuntimeException('Adres çözülemedi');
    foreach ($ips as $ip) {
        $genel = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
        if ($genel === false) throw new RuntimeException('İç ağ adreslerine istek yapılamaz');
    }

    $ch = curl_init($url->toAsciiString());
    curl_setopt_array($ch, [
        CURLOPT_RESOLVE        => ["{$host}:{$port}:{$ips[0]}"],  // kontrol edilen IP'ye sabitle
        CURLOPT_PROTOCOLS      => CURLPROTO_HTTP | CURLPROTO_HTTPS,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_MAXFILESIZE    => 2_000_000,
    ]);
    $yanit = curl_exec($ch);
    curl_close($ch);
    return (string)$yanit;
}

IPv6 adresini CURLOPT_RESOLVE'a verirken köşeli parantez içinde yazmanız gerekir; örneği kısa tutmak için atladım.

Ek savunma katmanları

  • Ağ katmanı: Web sunucusunun iç ağa ve meta veri adresine çıkışını güvenlik duvarında engelleyin. Uygulama kontrolü atlatılsa bile istek ulaşmaz.
  • Bulutta: AWS'de IMDSv2'yi zorunlu kılın; token gerektirdiği için basit SSRF ile meta veri okunamaz.
  • Kayıt: Reddedilen istekleri kaydedin; birinin iç adresleri denediğini görmek değerli bir alarm.
  • Yanıtı kullanıcıya olduğu gibi döndürmeyin: Resim indiriyorsanız gelen içeriğin gerçekten resim olduğunu doğrulayın.

Projelerinizde SSRF'ye nasıl yaklaşıyorsunuz? Özellikle DNS rebinding testleri için kullandığınız araçları paylaşabilirsiniz.

Kaynaklar

Yanıt yazmak için giriş yap. Giriş yap Hesap oluştur