Ele geçirilmiş web sitelerini incelerken en sık karşılaştığım giriş noktalarından biri, bir dosya yükleme formudur. Saldırgan resim yerine bir PHP dosyası yükler, sonra o dosyanın adresini tarayıcıda açar. Dosya çalışır ve saldırgan artık sunucuda istediği komutu çalıştırabilen bir "web shell"e sahiptir.
Bu saldırının önüne geçmek zor değildir. Ama birden fazla katmanın birlikte doğru yapılması gerekir.
Yanlış: Uzantıya ya da MIME türüne güvenmek
// YANLIŞ
if ($_FILES['resim']['type'] === 'image/jpeg') {
move_uploaded_file($_FILES['resim']['tmp_name'], 'uploads/' . $_FILES['resim']['name']);
}
Bu kodda üç ayrı sorun var:
$_FILES['resim']['type']değerini tarayıcı gönderir, yani saldırgan istediği değeri yazabilir.- Dosya adı da saldırganın kontrolündedir.
shell.php,resim.php.jpg,../../index.phpgibi adlar gönderilebilir. - Dosya, web sunucusunun PHP çalıştırdığı bir klasöre kaydediliyor.
Doğru yaklaşım: Katman katman
1. Dosya türünü içeriğe bakarak belirleyin
$izinli = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['dosya']['tmp_name']);
if (!isset($izinli[$mime])) {
throw new RuntimeException('Bu dosya türüne izin verilmiyor');
}
$uzanti = $izinli[$mime];
finfo, dosyanın ilk baytlarına bakarak gerçek türünü belirler. İzin listesi (whitelist) kullanın: Yalnızca kabul ettiğiniz türleri sayın. "PHP dosyası olmasın" gibi bir yasak listesi (blacklist) her zaman bir şeyi atlar: .phtml, .phar, .php5, .htaccess...
Ancak içerik kontrolü de tek başına yeterli değildir. Geçerli bir JPEG dosyasının sonuna PHP kodu eklenebilir; dosya hem geçerli bir resim hem de çalıştırılabilir bir betik olabilir. Bu yüzden sonraki katmanlar da şarttır.
2. Dosya adını siz belirleyin
$yeniAd = bin2hex(random_bytes(16)) . '.' . $uzanti;
Kullanıcının gönderdiği dosya adını asla diskte kullanmayın. Uzantıyı da kullanıcının dosya adından değil, yukarıda tespit ettiğiniz türden alın. Orijinal adı göstermeniz gerekiyorsa veritabanında ayrı bir sütunda saklayın ve gösterirken HTML olarak kaçırın.
3. Web kök dizininin dışına kaydedin
En güçlü önlem budur. Dosyaları web sunucusunun doğrudan sunamayacağı bir yere kaydedin:
/home/uygulama/
├── public_html/ ← web kök dizini
└── yuklemeler/ ← dosyalar burada, web'den erişilemez
Dosyayı göstermek ya da indirtmek gerektiğinde, yetkiyi kontrol eden küçük bir betik üzerinden sunun:
// indir.php?id=123
$dosya = $db->dosyaGetir((int)$_GET['id'], $kullanici->id); // sahiplik kontrolü
header('Content-Type: ' . $dosya['mime']);
header('Content-Disposition: attachment; filename="' . rawurlencode($dosya['orijinal_ad']) . '"');
header('X-Content-Type-Options: nosniff');
readfile('/home/uygulama/yuklemeler/' . $dosya['disk_adi']);
Bu yöntemin ek bir faydası da var: Bir kullanıcının faturasını, adresi tahmin eden başka bir kullanıcı indiremez.
4. Web kökünde tutmak zorundaysanız: Çalıştırmayı kapatın
WordPress'in uploads klasörü gibi, dosyaların web'den doğrudan sunulması gereken durumlarda, o klasörde betik çalıştırılmasını sunucu seviyesinde engelleyin.
Apache:
# uploads/.htaccess
<FilesMatch "\.(php|phtml|phar|php\d)$">
Require all denied
</FilesMatch>
location ~* ^/uploads/.*\.(php|phtml|phar)$ {
deny all;
}
5. Boyutu sınırlayın
if ($_FILES['dosya']['size'] > 5 * 1024 * 1024) {
throw new RuntimeException('Dosya en fazla 5 MB olabilir');
}
Ayrıca PHP'nin upload_max_filesize ve post_max_size ayarlarını ve web sunucusunun istek boyutu sınırını makul değerlere çekin. Sınırsız yükleme, diski doldurmaya dayalı bir saldırıya kapı açar.
6. Resimleri yeniden işleyin
Kullanıcıdan gelen resmi olduğu gibi saklamak yerine, GD ya da Imagick ile yeniden boyutlandırıp yeni bir dosya olarak kaydedin. Bu işlem resmin içine gizlenmiş fazladan veriyi büyük ölçüde temizler. Ayrıca fotoğraflardaki EXIF bilgilerini (çoğu zaman GPS konumu içerir) kaldırır, ki bu da kullanıcı gizliliği için önemlidir.
7. SVG ve HTML'e dikkat
SVG dosyaları bir resim gibi görünür, ama içinde JavaScript çalıştırabilir. Kullanıcıların yüklediği SVG dosyalarını sitenizin alan adından doğrudan sunmak, XSS açığına yol açabilir. SVG'ye gerçekten ihtiyacınız yoksa izin listesine eklemeyin. Gerekiyorsa bir temizleme kütüphanesinden geçirin ya da ayrı bir alan adından, Content-Disposition: attachment ile sunun. Aynı uyarı HTML dosyaları için de geçerlidir.
8. Zip ve arşivler
Kullanıcının yüklediği arşivleri sunucuda açıyorsanız iki tehlike vardır: Arşivin içindeki ../../ ile başlayan dosya adları (zip slip) ve küçük bir arşivin açıldığında gigabaytlarca yer kaplaması (zip bomb). Açmadan önce her dosyanın adını ve toplam boyutu kontrol edin.
Özet
- Türü içerikten belirleyin ve izin listesi kullanın.
- Dosya adını ve uzantıyı siz belirleyin.
- Mümkünse web kök dizininin dışına kaydedin ve yetki kontrolüyle sunun.
- Web kökündeyse betik çalıştırmayı kapatın.
- Boyutu sınırlayın, resimleri yeniden işleyin, SVG ve arşivlere ayrıca dikkat edin.
Bu katmanlardan yalnızca biri bile çoğu saldırıyı durdurur. Hepsi birlikte ise dosya yükleme formunu sunucunuzun en zayıf halkası olmaktan çıkarır.
Yorumlar
Henüz yorum yok. İlk yorumu siz yazın.