PHP'de SQL Injection: PDO ile Güvenli Sorgu Yazımı

PHP'de SQL Injection: PDO ile Güvenli Sorgu Yazımı

3 dakika okuma

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:

PHP5 satır
$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öner

Aynı 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:

PHP7 satır
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:

PHP7 satır
$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:

PHP9 satı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:

PHP10 satır
$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.

PHP6 satır
// 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

Bu form reCAPTCHA ile korunur; Google Gizlilik Politikası ve Hizmet Şartları geçerlidir.

İlk yorumu sen yap.

İlgili Yazılar