SQL injection, web uygulamalarına yönelik en eski ve en yıkıcı saldırılardan biridir; kullanıcıdan gelen verinin doğrudan bir SQL sorgusuna karıştırılmasıyla ortaya çıkar. Saldırgan, beklenen bir değer yerine sorgunun anlamını değiştiren bir metin göndererek veriyi okuyabilir, değiştirebilir, hatta tüm tabloyu silebilir. İyi haber şu: doğru kalıpları kullandığında bu saldırı sınıfını neredeyse tamamen kapatmak mümkündür. Bu yazıda nasıl çalıştığını gerçek örneklerle göreceğiz ve asıl savunma hattı olan prepared statement (parametreli sorgu) yaklaşımını adım adım kuracağız.
SQL injection nasıl çalışır?
Temel sorun, kodun veriyi ve komutu aynı metin içinde birleştirmesidir. Veritabanı, kendisine gelen metnin tamamını yorumlar; hangi kısmın "güvenilir kod", hangi kısmın "kullanıcı verisi" olduğunu bilmez. Klasik ve hatalı bir örnek:
// ASLA BÖYLE YAPMA
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
$result = $db->query($sql);
Burada kullanıcı email alanına normal bir adres yazarsa sorun yok. Ama girdi olarak ' OR '1'='1 gönderirse, sorgu şuna dönüşür:
SELECT * FROM users WHERE email = '' OR '1'='1'
'1'='1' her zaman doğru olduğu için sorgu bütün kullanıcıları döndürür. Kimlik doğrulama ekranında bu, parolasız giriş anlamına gelir.
Gerçek saldırı örnekleri
SQL injection tek bir biçimde gelmez. En sık karşılaşılan türleri tanımak, neden basit "tırnak temizleme" gibi yöntemlerin yetersiz kaldığını anlatır.
- Kimlik doğrulama atlatma: Yukarıdaki
OR '1'='1kalıbı, giriş formlarında en bilinen örnektir. - UNION tabanlı: Saldırgan, mevcut sorgunun sonucuna kendi
SELECTsonucunu ekler. Örneğin' UNION SELECT username, password FROM users --ile başka bir tablodan veri sızdırabilir. - Hata tabanlı (error-based): Veritabanının döndürdüğü ayrıntılı hata mesajları, tablo ve sütun yapısını ifşa eder; saldırgan bunu harita çıkarmak için kullanır.
- Kör (blind) injection: Uygulama hiç hata göstermese bile, doğru/yanlış cevaplara veya yanıt süresine bakarak veri bit bit çıkarılabilir (
AND SLEEP(5)gibi).
Dikkat edilmesi gereken nokta: girişi "tırnakları silerek" temizlemeye çalışmak kırılgan bir savunmadır. Farklı veritabanları, farklı kaçış kuralları ve kodlama hileleri vardır; eninde sonunda bir boşluk kalır. Doğru çözüm, veriyi koddan tamamen ayırmaktır.
Asıl savunma: prepared statement
Prepared statement (hazırlanmış/parametreli sorgu), sorgunun yapısını ve verisini birbirinden ayırır. Önce sorgu şablonunu yer tutucularla hazırlarsın, ardından değerleri ayrı bir kanaldan gönderirsin. Veritabanı, gelen değeri ne olursa olsun bir komut değil, salt bir değer olarak işler. PHP'de PDO ile:
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
Buradaki ? bir yer tutucudur. execute() içine verdiğin değer, sorgu yapısı zaten kilitlendikten sonra yerleştirilir; içinde tırnak, OR veya -- olması hiçbir şeyi değiştirmez. İsimli parametreler okunabilirliği artırır:
$stmt = $pdo->prepare(
'INSERT INTO posts (title, user_id) VALUES (:title, :user_id)'
);
$stmt->execute([':title' => $title, ':user_id' => $userId]);
PDO kullanıyorsan, sürücünün gerçek hazırlanmış sorgu kullandığından emin olmak için emülasyonu kapat:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Aynı ilke her dilde geçerlidir. Node.js'te mysql2 ile:
const [rows] = await conn.execute(
'SELECT id, name FROM users WHERE email = ?',
[email]
);
Anahtar kural tektir: kullanıcı verisini hiçbir zaman string birleştirme ile sorguya yapıştırma. Her zaman parametre olarak geçir.
ORM ve sorgu kurucular
Modern çatılar, doğru kullanıldığında parametreli sorguları otomatik üretir. Laravel'de Eloquent ve sorgu kurucu (query builder) bağlanan değerleri kendiliğinden parametre olarak işler:
// Güvenli — değer otomatik bağlanır
User::where('email', $email)->first();
DB::table('users')->where('email', $email)->get();
Ancak ORM seni otomatik korumaz; ham sorgu yazdığın anda sorumluluk yine sana geçer. DB::raw() içine veya whereRaw() argümanına kullanıcı verisini doğrudan koyarsan açık yeniden doğar. Bu durumda bile bağlama (binding) kullan:
// Yanlış: değer doğrudan gömülü
DB::select("SELECT * FROM users WHERE email = '$email'");
// Doğru: bağlama dizisi
DB::select('SELECT * FROM users WHERE email = ?', [$email]);
Tablo ve sütun adlarını nasıl güvenli kılarsın?
Yer tutucular yalnızca değerler için çalışır; tablo adı, sütun adı veya ORDER BY yönü gibi yapısal parçalar parametre olamaz. ORDER BY ? çalışmaz. Kullanıcı bir sütuna göre sıralama seçebiliyorsa, gelen değeri doğrudan sorguya koyma — sabit bir beyaz liste ile doğrula:
$allowed = ['name', 'created_at', 'price'];
$column = in_array($_GET['sort'], $allowed, true)
? $_GET['sort']
: 'name';
$sql = "SELECT * FROM products ORDER BY $column";
Burada kullanıcı yalnızca önceden onaylanmış değerlerden birini seçebilir; başka hiçbir metin sorguya giremez.
Açık kodu nasıl fark edersin?
Bir kod tabanını gözden geçirirken aklında basit bir soru tut: kullanıcıdan gelen herhangi bir değer, sorgu metnine birleştirme yoluyla giriyor mu? Şu işaretler neredeyse her zaman bir risktir:
- Sorgu içinde değişken interpolasyonu:
"... WHERE id = $id"ya da"... = " . $valuegibi birleştirmeler. $_GET,$_POST,$requestgibi kaynaklardan gelen verinin doğrudan sorguya gitmesi.whereRaw,DB::raw,query()veyaexec()çağrılarının yanında parametre dizisi olmaması.
Bu kalıpları aramak, çoğu açığı dakikalar içinde ortaya çıkarır. Bulduğun her noktayı parametreli sorguya çevir; tek istisna, beyaz liste ile doğrulanmış yapısal parçalardır.
Katmanlı savunma
Prepared statement, SQL injection'a karşı asıl ve yeterli savunmadır. Ama güvenlik bütüncül düşünülür; şu katmanlar saldırı yüzeyini daraltır ve olası hasarı sınırlar:
- En az ayrıcalık ilkesi: Uygulamanın veritabanı kullanıcısına yalnızca ihtiyaç duyduğu yetkileri ver. Web uygulamasının
DROP TABLEveyaGRANTyetkisi olmamalı. - Girdi doğrulama: Beklenen tipi ve formatı zorla (e-posta e-posta gibi, sayı sayı gibi). Bu injection'ı tek başına önlemez ama saldırı yüzeyini küçültür.
- Hata mesajlarını gizle: Üretimde ham veritabanı hatalarını kullanıcıya gösterme; logla ve genel bir mesaj döndür. Aksi halde error-based injection'a davetiye çıkarırsın.
- WAF ve izleme: Bir web uygulaması güvenlik duvarı bilinen saldırı kalıplarını filtreleyebilir; tek başına yeterli değildir ama ek bir katmandır.
Sık Sorulan Sorular
Girdiyi temizlemek (sanitize) SQL injection'ı önler mi?
Tek başına güvenilir değildir. Tırnak kaçışı ve kara liste filtreleri farklı veritabanlarında ve kodlama hilelerinde atlatılabilir. Asıl çözüm parametreli sorgudur; temizleme yalnızca tamamlayıcı bir önlemdir.
ORM kullanıyorum, yine de risk var mı?
Standart sorgularda ORM seni korur çünkü değerleri otomatik bağlar. Risk, ham SQL yazdığın (whereRaw, DB::raw, ham birleştirme) yerlerde doğar. Oralarda mutlaka bağlama parametreleri kullan.
Stored procedure kullanmak yeterli mi?
Tek başına değil. Bir stored procedure içinde dinamik SQL'i string birleştirerek kurarsan, açık prosedürün içine taşınmış olur. Prosedür içinde de parametreli sorgu mantığına uymak gerekir.
Uygulamanın güvenliğini şansa bırakma. Mevcut bir kod tabanını SQL injection açısından gözden geçirmemi veya güvenli bir veri katmanı kurmamı istersen, benimle iletişime geç ve sağlam bir temel atalım.