
PHP'de SQL Injection: PDO ile Güvenli Sorgu Yazımı
SQL injection, kullanıcıdan gelen verinin sorgu metnine doğrudan eklenmesiyle oluşur. Girdi tırnak içeriyorsa sorgunun anlamı değişir ve saldırgan koşulu kendi lehine çevirebilir. Açık genellikle arama kutuları, filtreler ve sıralama parametrelerinde görülür.
Sorunun kaynağı veri ile komutun aynı metin içinde taşınmasıdır. Çözüm de buradan çıkar: veri ile komut ayrı kanallardan gönderildiğinde girdi hiçbir koşulda komut olarak yorumlanmaz.
Açık nasıl oluşur?
Aşağıdaki sorgu, adres satırından gelen değeri doğrudan metne ekliyor. Girdi beklenen biçimde geldiğinde sorun görünmez, ancak tırnak içeren bir değer koşulu tamamen etkisiz hale getirir:
$sql = "SELECT * FROM users WHERE email = '" . $_GET['email'] . "'";
// gelen değer: x' OR '1'='1
// oluşan sorgu: SELECT * FROM users WHERE email = 'x' OR '1'='1'
// sonuç: tablodaki tüm satırlar dönerAynı yöntemle veri silinebilir, yeni kayıt eklenebilir ya da veritabanı sürümü gibi bilgiler dışarı çıkarılabilir. Açığın etkisi yalnızca okumayla sınırlı değildir.
Kaçış fonksiyonlarıyla çözmeye çalışmak güvenilir bir yol değildir. Karakter kodlaması, sayısal alanlar ve iç içe sorgular gibi durumlarda kaçış yetersiz kalır. Bu yüzden tek doğru yaklaşım hazır sorgudur.
Hazır sorgu ile parametre bağlama
Hazır sorguda önce yalnızca sorgu şablonu gönderilir, değerler ayrı olarak iletilir. Veritabanı şablonu bir kez çözümler ve değerleri her zaman veri olarak işler:
function kullaniciGetir(PDO $db, string $eposta): ?array
{
$stmt = $db->prepare('SELECT id, eposta, rol FROM users WHERE eposta = ?');
$stmt->execute([$eposta]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}Parametre sayısı arttığında isimli yer tutucular okunurluğu artırır. Aynı değerin birden fazla yerde kullanılması gerektiğinde de isimli yer tutucu tekrar bağlamayı gereksiz kılar:
$stmt = $db->prepare(
'SELECT * FROM siparisler
WHERE kullanici_id = :kid AND durum = :durum
ORDER BY olusturuldu DESC'
);
$stmt->execute(['kid' => $kullaniciId, 'durum' => 'aktif']);Parametre bağlanamayan yerler
Yer tutucular yalnızca değerler için çalışır. Tablo adı, kolon adı, sıralama yönü ve LIMIT gibi yapısal ögeler bağlanamaz. Bu alanlar kullanıcıdan geliyorsa beyaz liste kullanılmalıdır:
$izinliKolonlar = ['ad', 'olusturuldu', 'tutar'];
$kolon = in_array($_GET['sirala'] ?? '', $izinliKolonlar, true)
? $_GET['sirala']
: 'olusturuldu';
$yon = ($_GET['yon'] ?? '') === 'asc' ? 'ASC' : 'DESC';
$limit = min(max((int) ($_GET['limit'] ?? 20), 1), 100);
$sql = "SELECT * FROM kayitlar ORDER BY {$kolon} {$yon} LIMIT {$limit}";Beyaz listenin mantığı şudur: kullanıcının gönderdiği değer sorguya girmez, yalnızca önceden tanımlı seçeneklerden birini seçmek için kullanılır. Sayısal değerler ise tip dönüşümüyle güvenli hale getirilir.
PDO bağlantısının doğru kurulumu
Varsayılan ayarlarla PDO, hazır sorguları veritabanına göndermek yerine kendisi taklit edebilir. Taklit modu kapatıldığında sorgu gerçekten sunucu tarafında hazırlanır. Hata modunun istisna olarak ayarlanması ise başarısız sorguların sessizce geçilmesini engeller:
$db = new PDO(
'mysql:host=localhost;dbname=uygulama;charset=utf8mb4',
$kullanici,
$parola,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);Karakter kümesinin bağlantı satırında belirtilmesi de güvenlik açısından önemlidir. Uyumsuz kodlama, bazı karakterlerin kaçış mekanizmasını atlatmasına imkan tanır.
Veritabanı kullanıcısının yetkisini sınırlamak
Uygulamanın bağlandığı veritabanı kullanıcısı yalnızca ihtiyaç duyduğu işlemleri yapabilmelidir. Yalnızca okuma yapan bir rapor modülünün yazma yetkisi olmasına gerek yoktur. Bu sınırlama açığı kapatmaz, ancak açık kullanıldığında verilebilecek zararı küçültür.
- Uygulama kullanıcısına şema değiştirme yetkisi verilmez.
- Yedekleme ve raporlama için ayrı, salt okunur kullanıcı tanımlanır.
- Veritabanı sunucusu yalnızca uygulama sunucusundan gelen bağlantıları kabul eder.
Sorgu üreticileri ve ORM
Sorgu üreticileri ve ORM katmanları parametreleri kendiliğinden bağlar, bu yüzden doğru kullanıldıklarında koruma sağlarlar. Ancak ham sorgu çalıştırma yöntemleri hepsinde bulunur ve bu yöntemler kullanıldığında sorumluluk yine geliştiriciye geçer.
Kod incelemesinde aranacak işaret şudur: sorgu metninde birleştirme operatörü ya da değişken enterpolasyonu görünüyorsa, o satır ayrıca incelenmelidir.
Açığı erken yakalamak
Kod incelemesi tek başına yeterli değildir; gözden kaçan tek satır açığın kalmasına yeter. Statik analiz araçları, kullanıcı girdisinin sorguya kadar izlediği yolu takip ederek bu satırları otomatik işaretler.
İkinci savunma katmanı testtir. Bilinen saldırı dizelerini içeren girdilerle çalışan bir test kümesi, sonradan eklenen kodun aynı hatayı tekrarlamasını engeller. Bu testler sürekli tümleştirme akışına eklendiğinde koruma kalıcı hale gelir.
// test girdisi olarak kullanılabilecek örnek dizeler
$denemeler = [
"x' OR '1'='1",
"'; DROP TABLE users; --",
"1 UNION SELECT null, version(), null",
];Kontrol listesi
- Tüm değerler hazır sorgu ile bağlanır.
- Yapısal ögeler beyaz listeden seçilir.
- Sayısal parametreler tip dönüşümünden geçirilir.
- PDO taklit modu kapatılır, hata modu istisna olarak ayarlanır.
- Veritabanı kullanıcısına yalnızca gereken yetkiler verilir.
- Ham sorgu çalıştıran satırlar ayrıca gözden geçirilir.
İlgili yazılar
Yorumlar
İlk yorumu sen yap.
İlgili Yazılar

PHP Uygulamasını Üretime Almak: Yapılandırma ve Güvenlik
Geliştirme makinesinde çalışan kod ile üretimde güvenle çalışan kod arasındaki fark yapılandırmadır. Ayarlar, izinler, dağıtım akışı ve son kontroller.

PHP ile REST API Yazmak: Yönlendirme ve HTTP Durum Kodları
Bir API'nin kalitesi çoğunlukla ayrıntılarda belli olur: doğru durum kodu, tutarlı hata biçimi ve öngörülebilir adresler. Saf PHP ile uçtan uca kurulum.

PHPUnit ile Birim Test Yazmak: Kurulum, Sahte Nesneler ve Kapsam
Testin amacı hata bulmak değil, değişikliği güvenle yapabilmektir. Kurulum, ilk test, sahte nesneler ve test edilebilir kodun özellikleri.