OWASP Top 10:2025'e yeni giren A10 – Mishandling of Exceptional Conditions kategorisi, uzun süredir sahada gördüğümüz ama adı konmamış bir hata ailesini kapsıyor: uygulama beklenmedik bir durumla karşılaştığında yanlış yöne düşmesi. Bu konuda en sık gördüğüm üç kalıbı ve düzeltmelerini örnek kodla paylaşıyorum.
1. "Fail open": hata olunca izin vermek
Yetki kontrolü bir dış servise, veritabanına ya da önbelleğe bağlıysa ve o servis hata verirse ne oluyor?
// YANLIŞ: yetki servisi çökerse herkes içeri girer
function canEdit(int $userId, int $postId): bool {
try {
return $this->acl->check($userId, 'edit', $postId);
} catch (\Throwable $e) {
error_log($e->getMessage());
return true; // "kullanıcıyı mağdur etmeyelim"
}
}
// DOĞRU: hata durumunda reddet
function canEdit(int $userId, int $postId): bool {
try {
return $this->acl->check($userId, 'edit', $postId);
} catch (\Throwable $e) {
error_log('ACL hatası: ' . $e->getMessage());
return false;
}
}
Aynı kalıp hız sınırlayıcılarda da görülür: Redis'e ulaşılamayınca sınırlamanın tamamen devre dışı kalması, kaba kuvvet saldırısı için tam fırsattır.
2. Hata mesajında iç bilgi sızdırmak
SQL hatası, dosya yolu, yığın izi ya da sunucu sürümü kullanıcıya gösterilmemeli. Doğrusu: kullanıcıya genel bir mesaj, kayıt dosyasına ayrıntı.
try {
$router->dispatch();
} catch (\Throwable $e) {
error_log(get_class($e) . ': ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
http_response_code(500);
include 'views/errors/500.php'; // ayrıntı yok
}
PHP'de üretim ortamında display_errors=0, log_errors=1 olmalı. Ayrıntıyı görmek isteyen yöneticiye bile yalnızca kısa bir özet gösterin, yığın izini değil.
3. Yarım kalan işlemler
Para transferi, puan aktarma, stok düşme gibi çok adımlı işlemlerde bir adım hata verirse önceki adımlar geri alınmalı. Aksi halde saldırgan hatayı bilerek tetikleyip tutarsız durumdan yararlanabilir (örneğin puanın düşülmeden ödülün verilmesi).
$db->beginTransaction();
try {
$db->execute('UPDATE users SET points = points - ? WHERE id = ? AND points >= ?', [$cost, $uid, $cost]);
if ($db->rowCount() !== 1) throw new RuntimeException('Yetersiz puan');
$db->execute('INSERT INTO rewards (user_id, item) VALUES (?, ?)', [$uid, $item]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Kod incelemesinde sorulacak sorular
- Her
catchbloğunda sonuç ne? İşlem reddediliyor mu, yoksa sessizce devam mı ediliyor? - Boş
catch {}blokları var mı? - Zaman aşımı, boş yanıt, beklenmeyen tür (null, dizi yerine metin) durumları test ediliyor mu?
- Kullanıcıya dönen hata mesajları kimlik, yol ya da sürüm bilgisi içeriyor mu?
Kendi kodunuzda karşılaştığınız "fail open" örneklerini (anonimleştirerek) paylaşırsanız, iyi bir eğitim arşivi olur.
