Ana içeriğe geç
  1. Blogs/

Cloudflare Workers İçin SSRF Guard Yazdım, Üç Yoldan Atlatılabiliyordu

·575 kelime·3 dk
appsec cloudflare ssrf security

Cloudflare Workers üzerinde küçük bir uptime monitor yapıyorum. Uygulama kullanıcının verdiği URL’leri fetch ediyor, yani tasarımı gereği bir SSRF açığına davetiye çıkarıyor — tabii siz önlem almadıysanız.

İlk yazdığım guard mantıklı duruyordu: URL’i parse et, açık bir private IP ise reddet, değilse fetch et. Kendi kodumu saldırgan gözüyle on dakika incelemem, üç ayrı atlatma yolu bulmama yetti. Hiçbiri özel bir teknik gerektirmiyor, çoğu ev yapımı SSRF guard’ının düştüğü klasik hatalar. Kendimi saldırsaydım hangi sırayla bulacaksam o sırayla anlatayım.

1. AAAA’yı unutup sadece A kaydına bakmak #

Guard hostname’i çözüyor, çıkan IPv4 adresini private-range listesiyle karşılaştırıyor, sorun yoksa devam ediyordu. Hedef AAAA-only cevap verdiği anda bu yaklaşım çöküyor: ::1, ya da fc00::/7 aralığındaki herhangi bir ULA (IPv6’nın RFC1918 karşılığı). A kaydı hiç yok, dolayısıyla sadece A’ya bakan kontrol itiraz edecek bir şey bulamıyor ve doğruca loopback’e ya da internal bir IPv6 host’a gidiyor.

Çözüm basit: karar vermeden önce hem A hem AAAA’yı çözün. Workers’ta native resolver olmadığından DoH en pratik yol — A için cloudflare-dns.com/dns-query?type=1, AAAA için type=28 (ikinci bir sağlayıcı istiyorsanız dns.google/resolve aynı işi görür).

2. İlk adımı kontrol edip sonraki redirect’lere güvenmek #

Kullanıcının verdiği URL’i kontrol etmek şart ama yetmiyor. Redirect’leri takip eden bir fetch(), temiz görünen public bir host’tan ikinci adımda rahatlıkla internal bir host’a geçebilir. Location header’ını string olarak kontrol etmek (scheme’e bakmak, belli private literal’leri reddetmek) kapsıyormuş gibi hissettiriyor ama kapsamıyor: redirect’in gittiği hostname tamamen public görünüp yine de private bir IP’ye çözülebiliyor.

Çözüm: resolve edilmiş IP kontrolünü, bağlantıyı asıl açan fonksiyonun içine koyun ve bunu sadece kullanıcının girdiği host’ta değil, her redirect adımında çalıştırın.

3. new URL()‘e fazla güvenmek — ama sadece literal’ler için işe yarıyor #

WHATWG’nin URL parser’ı decimal, octal ve hex IP gösterimlerini kendisi normalize ediyor: http://2130706433, http://0177.0.0.1, http://0x7f.1 — üçü de sonunda 127.0.0.1 oluyor. Bu gerçekten işe yarıyor: parse’tan sonra bir isPrivateIpv4() kontrolü koyarsanız, literal’lere uygulanan obfuscation’ların hepsini bedavaya yakalarsınız.

Tuzak şurada: bu rahatlığı hostname’lere de genellemek. URL, string’in içinde gördüğü literal’i normalize ediyor, hostname’in neye çözüldüğünü bilemez. “URL’i normalize ediyorum, demek ki güvendeyim” diye düşünüyorsanız, zaten regex’le kolayca yakalanacak kısmı kapatıp asıl mesele olan DNS tabanlı bypass’ı ardına kadar açık bırakmışsınız demektir.

Tam kapatamadığım tek şey — ve neden kapatmaya çalışmadım #

Üç sorunu da düzeltseniz bile bir açık kalıyor: fetch() bağlantı anında DNS’i yeniden çözüyor, Workers ise arbitrary host’lar için IP pinleme imkanı vermiyor (cf.resolveOverride sadece kendi zone’unuzdaki host’larda işe yarıyor). Sizin resolve kontrolünüzle asıl fetch arasına giren TTL-0 bir DNS-rebind, teoride hâlâ açık.

Bunu kapatmaya uğraşmak yerine kabul edilmiş risk olarak yazdım, çünkü somut bir sebebi var: Cloudflare’in edge egress’i RFC1918 alanına hiç gitmiyor, AWS’deki 169.254.169.254 gibi sabit bir tenant metadata endpoint’i de yok. Yani başarılı bir rebind’in gerçek etkisi neredeyse sıfır. Kalan bir riski “kabul ediyorum, işte sebebi” diye yazmak, onu kapalıymış gibi göstermekten çok daha dürüst.

TL;DR — kullanıcının verdiği URL’i fetch eden bir şey yazıyorsanız #

  • Karar vermeden önce hem A hem AAAA’yı çözün.
  • Resolve edilen IP’yi bağlantının açıldığı noktada kontrol edin — ilk host’ta da, her redirect adımında da. Bir kere, en başta yapmak yetmiyor.
  • new URL() normalizasyonu sadece obfuscate edilmiş IP literal’lerini yakalar. Hostname’in kötü bir yere çözülmesine karşı hiçbir işe yaramaz. Biri sizi diğeri konusunda da güvende hissettirmesin.
  • Platformunuz DNS-rebind TOCTOU’yu tam kapatamıyorsa bunu açıkça yazın, kalan riski neden kabul ettiğinizi de. Üstünü örtmeyin.

Yan iş olarak web uygulaması sızma testleri yapıyorum — SSRF, auth mantığı ve OWASP top 10’un geri kalanı. Kullanıcı girdisiyle URL fetch eden bir şey yazıyorsanız ve ikinci bir göz isterseniz: [email protected].