Teknoloji, Bilim ve Akıl
PHP ile SQL Injection Nasıl Önlenir? PDO Hazır İfadeler Rehberi
Web Programlama

PHP ile SQL Injection Nasıl Önlenir? PDO Hazır İfadeler Rehberi

5 dk okuma 3 okunma 0 yorum
Kısaca: PHP projelerinde SQL injection, kullanıcı verisini sorgu metnine doğrudan eklemek yerine PDO hazır ifadeleri ve parametre bağlama kullanılarak önlenir. Bu yöntemde veri ile sorgu komutu birbirinden tamamen ayrılır.
İçindekiler
  1. SQL Injection Nedir?
  2. SQL Injection Saldırısı Nasıl Çalışır?
  3. Neden addslashes veya Kaçış Fonksiyonları Yeterli Değildir?
  4. PDO ile Güvenli Bağlantı Nasıl Kurulur?
  5. Hazır İfadeler Nasıl Kullanılır?
  6. Hazır İfadelerde Özel Durumlar Nelerdir?
  7. LIKE Araması Nasıl Güvenli Yapılır?
  8. IN Listesi Nasıl Bağlanır?
  9. ORDER BY ve Sütun Adları İçin Beyaz Liste
  10. LIMIT ve OFFSET Nasıl Bağlanır?
  11. Güvensiz ve Güvenli Yaklaşımların Karşılaştırması
  12. Hazır İfadelerin Yanında Hangi Ek Önlemler Alınmalı?
  13. Sahadan Gözlem: Kayıt Dosyaları Ne Anlatıyor?

PHP ile SQL injection saldırısını önlemenin en güvenilir yolu, kullanıcıdan gelen hiçbir veriyi SQL sorgusunun metnine doğrudan eklememek ve bunun yerine PDO hazır ifadelerini (prepared statements) kullanmaktır. Hazır ifadelerde sorgu kalıbı veritabanına önce gönderilir, kullanıcı verisi ise ayrı bir parametre olarak iletilir. Veritabanı bu veriyi hiçbir zaman komut olarak yorumlamaz.

Bu yazıda SQL injection saldırısının mantığını, açık barındıran tipik PHP kodlarını, PDO ile güvenli bağlantı kurmayı ve LIKE, IN, ORDER BY, LIMIT gibi hazır ifadelerin zorlandığı özel durumları örnek kodlarla ele alacağız.

SQL Injection Nedir?

SQL injection, saldırganın bir form alanına, URL parametresine veya çerez değerine SQL kodu yerleştirerek uygulamanın çalıştırdığı sorgunun yapısını değiştirmesidir. Sonuç olarak saldırgan yetkisi olmadığı verileri okuyabilir, oturum açma ekranını atlatabilir, kayıtları değiştirebilir veya silebilir.

Saldırının temel sebebi tektir: uygulama, kullanıcı verisini güvenilir kabul edip SQL metninin bir parçası haline getirir. Veritabanı açısından hangi kısmın geliştirici tarafından, hangi kısmın kullanıcı tarafından yazıldığı ayırt edilemez.

SQL Injection Saldırısı Nasıl Çalışır?

Aşağıdaki oturum açma kodu, eski PHP projelerinde hâlâ sıkça karşılaşılan açık bir örnektir:

<?php
// GÜVENSİZ ÖRNEK - kullanmayın
$eposta = $_POST['eposta'];
$sifre  = $_POST['sifre'];

$sql = "SELECT * FROM kullanicilar
        WHERE eposta = '$eposta' AND sifre = '$sifre'";
$sonuc = $db->query($sql);

Saldırgan e-posta alanına ' OR '1'='1' -- ifadesini yazdığında oluşan sorgu şu hale gelir:

SELECT * FROM kullanicilar
WHERE eposta = '' OR '1'='1' -- ' AND sifre = ''

Koşul her satır için doğru olur, şifre kontrolü ise yorum satırına dönüşür. Uygulama ilk kullanıcıyla, çoğu zaman da yönetici hesabıyla oturum açar. Aynı mantıkla UNION kullanılarak başka tablolardaki veriler de okunabilir.

Neden addslashes veya Kaçış Fonksiyonları Yeterli Değildir?

Eski projelerde addslashes() veya mysqli_real_escape_string() ile tırnak karakterlerini kaçırmak yaygın bir alışkanlıktır. Bu yöntemler bazı durumlarda koruma sağlasa da güvenilir bir savunma değildir.

  • Tırnak içine alınmamış sayısal parametrelerde kaçış işe yaramaz. WHERE id = $id sorgusunda saldırgan tırnak kullanmadan 1 OR 1=1 gönderebilir.
  • Karakter seti uyuşmazlıklarında kaçış atlatılabilir.
  • Geliştiricinin tek bir alanda kaçışı unutması, tüm uygulamayı açık hale getirir.

Hazır ifadeler ise yapı gereği güvenlidir. Geliştiricinin her alanı hatırlamasına bağlı değildir.

PDO ile Güvenli Bağlantı Nasıl Kurulur?

Güvenli bir yapının ilk adımı, PDO bağlantısını doğru seçeneklerle oluşturmaktır. MySQL için örnek:

<?php
$dsn = 'mysql:host=localhost;dbname=magaza;charset=utf8mb4';

try {
    $pdo = new PDO($dsn, 'db_kullanici', 'db_sifre', [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ]);
} catch (PDOException $e) {
    error_log($e->getMessage());
    exit('Veritabanı bağlantısı kurulamadı.');
}

SQLite kullanan projelerde bağlantı daha kısadır:

<?php
$pdo = new PDO('sqlite:' . __DIR__ . '/veri/site.db', null, null, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

Seçeneklerin anlamları şöyledir:

SeçenekNe İşe Yarar?
ERRMODE_EXCEPTIONHataları istisna olarak fırlatır, sessizce yutulan hataları önler
FETCH_ASSOCSonuçları yalnızca sütun adlarıyla dizi olarak döndürür
EMULATE_PREPARES falseMySQL tarafında gerçek hazır ifadelerin kullanılmasını sağlar
charset=utf8mb4Karakter seti uyuşmazlığından doğan riskleri azaltır ve Türkçe karakterleri doğru saklar

Hata mesajının kullanıcıya gösterilmemesi de önemlidir. Veritabanı hata mesajları tablo adları ve sorgu yapısı hakkında saldırgana ipucu verir. Bu yüzden hata kayıt dosyasına yazılır, kullanıcıya genel bir mesaj gösterilir.

Hazır İfadeler Nasıl Kullanılır?

PDO iki tür yer tutucu destekler: soru işareti ile konumsal ve iki nokta ile adlandırılmış yer tutucular. Yukarıdaki güvensiz oturum açma kodunun güvenli hali şöyledir:

<?php
$stmt = $pdo->prepare('SELECT id, ad, sifre_hash FROM kullanicilar WHERE eposta = :eposta LIMIT 1');
$stmt->execute([':eposta' => $_POST['eposta'] ?? '']);
$kullanici = $stmt->fetch();

if ($kullanici && password_verify($_POST['sifre'] ?? '', $kullanici['sifre_hash'])) {
    session_regenerate_id(true);
    $_SESSION['kullanici_id'] = $kullanici['id'];
} else {
    $hata = 'E-posta veya şifre hatalı.';
}

Bu örnekte iki önemli değişiklik vardır. İlki, e-posta değerinin parametre olarak gönderilmesidir. İkincisi, şifrenin sorguda karşılaştırılmaması ve password_hash() ile saklanan özet değerinin password_verify() ile doğrulanmasıdır. Şifreyi düz metin olarak saklamak, SQL injection kadar ciddi bir güvenlik hatasıdır.

Konumsal yer tutucu ile kayıt ekleme örneği:

<?php
$stmt = $pdo->prepare('INSERT INTO yorumlar (yazi_id, ad, icerik, tarih) VALUES (?, ?, ?, ?)');
$stmt->execute([
    (int) $_POST['yazi_id'],
    trim($_POST['ad']),
    trim($_POST['icerik']),
    date('Y-m-d H:i:s'),
]);

Hazır İfadelerde Özel Durumlar Nelerdir?

Hazır ifadeler yalnızca değerleri parametre olarak alabilir. Tablo adı, sütun adı, sıralama yönü gibi SQL yapısının parçaları parametre olarak bağlanamaz. Bu durumlar ayrı çözüm ister.

LIKE Araması Nasıl Güvenli Yapılır?

Joker karakterler sorgu metnine değil, parametre değerine eklenir:

<?php
$arama = $_GET['q'] ?? '';
$stmt = $pdo->prepare('SELECT id, baslik FROM yazilar WHERE baslik LIKE :q');
$stmt->execute([':q' => '%' . $arama . '%']);

Kullanıcının yüzde veya alt çizgi karakteri girerek tüm kayıtları listelemesini istemiyorsanız bu karakterleri ayrıca kaçırıp sorguya ESCAPE ifadesi ekleyebilirsiniz.

IN Listesi Nasıl Bağlanır?

Değişken sayıda değer içeren IN listelerinde yer tutucular dinamik olarak üretilir:

<?php
$idler = array_map('intval', $_POST['secilenler'] ?? []);

if ($idler) {
    $yerTutucu = implode(',', array_fill(0, count($idler), '?'));
    $stmt = $pdo->prepare("SELECT * FROM urunler WHERE id IN ($yerTutucu)");
    $stmt->execute($idler);
    $urunler = $stmt->fetchAll();
}

Burada sorgu metnine eklenen tek şey soru işaretleridir. Kullanıcı verisi yine parametre olarak gider.

ORDER BY ve Sütun Adları İçin Beyaz Liste

Sıralama sütunu kullanıcıdan geliyorsa izin verilen değerler önceden tanımlanır ve yalnızca bu listeden seçim yapılır:

<?php
$izinliSutunlar = [
    'ad'    => 'ad',
    'fiyat' => 'fiyat',
    'tarih' => 'olusturma_tarihi',
];

$sutun = $izinliSutunlar[$_GET['sirala'] ?? ''] ?? 'olusturma_tarihi';
$yon   = (($_GET['yon'] ?? '') === 'artan') ? 'ASC' : 'DESC';

$stmt = $pdo->query("SELECT id, ad, fiyat FROM urunler ORDER BY $sutun $yon");

Kullanıcı hangi değeri gönderirse göndersin, sorguya yalnızca geliştiricinin yazdığı sütun adları girer.

LIMIT ve OFFSET Nasıl Bağlanır?

Sayfalama değerlerinin tamsayı olarak bağlanması gerekir. Aksi halde bazı yapılandırmalarda değer metin olarak gönderilir ve sorgu hata verir:

<?php
$sayfa  = max(1, (int) ($_GET['sayfa'] ?? 1));
$limit  = 20;
$offset = ($sayfa - 1) * $limit;

$stmt = $pdo->prepare('SELECT id, baslik FROM yazilar ORDER BY id DESC LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();

Güvensiz ve Güvenli Yaklaşımların Karşılaştırması

DurumGüvensiz YaklaşımGüvenli Yaklaşım
Değer filtrelemeDeğişkeni sorgu metnine eklemekprepare ve execute ile parametre bağlamak
Metin aramaLIKE içine değişkeni yazmakJoker karakteri parametre değerine eklemek
Çoklu değerimplode ile değerleri birleştirmekDinamik soru işareti üretmek
Sıralama sütunuGET parametresini doğrudan kullanmakBeyaz liste ile eşleştirmek
SayfalamaDeğeri metin olarak eklemekPARAM_INT ile bağlamak

Hazır İfadelerin Yanında Hangi Ek Önlemler Alınmalı?

Hazır ifadeler SQL injection saldırısına karşı temel savunmadır, ancak katmanlı güvenlik yaklaşımı her zaman daha sağlamdır.

  • En az yetki ilkesi: Uygulamanın kullandığı veritabanı kullanıcısına yalnızca ihtiyaç duyduğu tablolarda gereken yetkileri verin. DROP veya GRANT yetkisi gerekmez.
  • Girdi doğrulama: Sayı beklenen alanı tamsayıya çevirin, e-posta alanını filter_var() ile doğrulayın.
  • Hata gösterimini kapatma: Canlı sunucuda display_errors kapalı olmalı, hatalar kayıt dosyasına yazılmalıdır.
  • Güncel PHP sürümü: Desteği bitmiş sürümler başka açıklar barındırabilir.
  • Çıktı kaçışı: Veritabanından okunan veriyi ekrana basarken htmlspecialchars() kullanarak XSS riskini de kapatın.

Sahadan Gözlem: Kayıt Dosyaları Ne Anlatıyor?

Yönettiğimiz sitelerin sunucu erişim kayıtlarına bakıldığında, arama kutularına ve ID parametrelerine otomatik tarama araçlarıyla gönderilmiş SQL injection denemeleri düzenli olarak görülür. Parametrelere tek tırnak, UNION SELECT, SLEEP gibi ifadeler eklenmiş istekler, sitenin küçük veya büyük olmasından bağımsız olarak gelir.

Bu denemeler, eski usul yazılmış yönetim panellerinde ve uzun süredir dokunulmamış yardımcı sayfalarda başarılı olur. Bir projeyi devraldığımızda ilk yaptığımız işlerden biri, kod tabanında query( çağrılarını ve sorgu metnine birleştirilen değişkenleri aramak ve tamamını hazır ifadelere dönüştürmektir. Tek bir unutulmuş sayfanın tüm veritabanını riske atabileceğini unutmamak gerekir.

Sık Sorulan Sorular

mysqli ile de hazır ifade kullanılabilir mi?
Evet. mysqli eklentisi de prepare() ve bind_param() ile hazır ifadeleri destekler. Ancak PDO, SQLite ve MySQL gibi farklı veritabanlarında aynı sözdizimiyle çalıştığı ve adlandırılmış parametreleri desteklediği için yeni projelerde genellikle daha pratiktir.
Hazır ifadeler performansı düşürür mü?
Hayır, pratikte fark edilir bir performans kaybı yoktur. Aynı sorgu farklı değerlerle tekrar tekrar çalıştırıldığında hazır ifade yeniden kullanılabildiği için bazı durumlarda daha verimli bile olabilir.
Tablo adını parametre olarak bağlayabilir miyim?
Hayır. Hazır ifadeler yalnızca değerleri bağlar. Tablo ve sütun adları gibi yapısal öğeler için izin verilen değerlerden oluşan bir beyaz liste kullanmak gerekir.
SQLite kullanan projelerde SQL injection riski var mı?
Evet. SQL injection veritabanı türüne değil, sorgunun nasıl oluşturulduğuna bağlıdır. SQLite kullanan bir PHP projesinde de kullanıcı verisi sorgu metnine eklenirse aynı saldırılar mümkündür.
Sadece POST ile gelen verileri korumak yeterli mi?
Hayır. GET parametreleri, çerezler, HTTP başlıkları ve hatta veritabanından okunup yeniden sorguda kullanılan değerler de saldırı kaynağı olabilir. Sorguya giren her dış veri parametre olarak bağlanmalıdır.
Ramazan Yeşildal
Ramazan Yeşildal

Karaman merkezli bilgi teknolojileri yöneticisi ve bağımsız yazılım geliştirici.

0 Yorum

Yorum yazın

E-posta adresiniz yayınlanmaz. Yorumlar onaydan sonra görünür.