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ış
- Adresi standart bir ayrıştırıcıyla ayrıştırın, yalnızca
http/httpskabul edin. - 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.
- İsteği, kontrol ettiğiniz IP'ye sabitleyerek yapın (DNS rebinding'e karşı).
- 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.
