HG PHP GüvenliğiEl Kitabı
Bölümler 06 / 38

İçindekiler/Enjeksiyon Sınıfı Açıklar

Bölüm 06

SQL Injection

16 dk okuma3.324 kelimePHP 8.1+
Ön koşul

Bölüm 1 (Güven sınırı, type juggling) ve temel SQL bilgisi.

Bu bölümün kazanımı

SQL Injection'ı ezberlenmiş bir "kaçış fonksiyonu" olarak değil, bir bağlam karışması problemi olarak kavrayacak; her varyantını (klasik, kör, ikinci derece, kolon/ORDER BY bağlamı) aynı kök nedene indirgeyebileceksiniz.

1. Giriş

SQL Injection (SQLi), 1998'de Jeff Forristal'ın "rain.forest.puppy" takma adıyla Phrack dergisinde yayımladığı bir yazıyla geniş kitleye tanıtıldı. Aradan çeyrek asır geçmesine ve savunmasının teknik olarak tamamen çözülmüş olmasına rağmen, OWASP Top 10 listesinde onlarca yıldır yer alan bir açık sınıfıdır. 2021 listesinde "Injection" başlığı altında üçüncü sıradadır.

Bu paradoks öğreticidir: SQLi teknik olarak çözülmüştür (parametreli sorgular), ama kültürel olarak çözülmemiştir. Her yıl on binlerce yeni geliştirici, dize birleştirmeyle sorgu kuran eski öğreticilerden öğrenmeye devam eder. Legacy kod tabanları, güvenli olmayan kalıpları taşımaya devam eder. ORM'ler bile güvenli olmayan "raw query" kaçış kapıları sunar.

SQLi'nin etkisi nadiren "birkaç satır veri" ile sınırlıdır. Doğru koşullarda tüm veritabanının okunmasına, kimlik doğrulamanın atlanmasına, verinin değiştirilmesine, hatta (veritabanı yapılandırmasına bağlı olarak) dosya sistemine yazmaya ve uzaktan kod çalıştırmaya kadar tırmanır. Tarihin en büyük veri ihlallerinin birçoğunun kökeninde tek bir doğrulanmamış parametre vardır.


2. Temel Teori

SQL Injection'ın kök nedeni tek bir cümledir: Kod (SQL komutu) ile veri (kullanıcı girdisi) aynı dize içinde karıştırılır ve veritabanı bu dizeyi ayrıştırırken, verinin bir kısmını komut olarak yorumlar.

Sorunu katman katman inceleyelim.

Dil seviyesinde (PHP): PHP için bir SQL sorgusu yalnızca bir dizedir (string). PHP, o dizenin içinde geçerli SQL olup olmadığını bilmez, umursamaz. Dize birleştirme (.) ile kullanıcı girdisini sorguya eklediğinizde, PHP hiçbir sınır tanımaz — komutla veri birbirine karışır.

$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];

PHP için bu, id = 5 ile id = 5 OR 1=1 arasında hiçbir anlam farkı olmayan iki dizedir.

Protokol seviyesinde (HTTP): Girdi, güven sınırının dış tarafından — sorgu dizesi, form gövdesi, çerez, başlık — gelir. Saldırgan bu alanların herhangi birine SQL meta-karakterleri yerleştirebilir. HTTP, içeriğin ne olması gerektiğine dair hiçbir kısıtlama getirmez.

Veri katmanında (veritabanı): Veritabanı sunucusu, kendisine gelen dizeyi bir ayrıştırıcıdan (parser) geçirir. Bu ayrıştırıcı, ' (tırnak), -- (yorum), ; (ifade sonu), UNION, OR gibi meta-karakterlere ve anahtar kelimelere özel anlam yükler. Kritik olan şudur: Ayrıştırıcı, dizenin hangi kısmının programcının niyeti, hangi kısmının kullanıcı verisi olduğunu bilemez. Onun için gelen tek bir metin akışı vardır.

İşte bu yüzden mysqli_real_escape_string gibi kaçış (escaping) fonksiyonları yetersiz bir savunmadır: onlar yalnızca dize bağlamındaki ' gibi karakterleri kaçırır. Ama girdi bir tırnak içine alınmamışsa (sayısal bağlam), bir kolon adıysa, bir ORDER BY yönüyse veya bir LIMIT değeriyse, kaçış hiçbir işe yaramaz.

Çözümün özü — ayrıştırma sınırı: Tek doğru çözüm, komutla veriyi fiziksel olarak ayırmaktır. Parametreli sorgular (prepared statements) tam olarak bunu yapar: SQL şablonu ve parametreler veritabanına iki ayrı kanaldan gönderilir. Veritabanı önce şablonu ayrıştırıp sorgu planını sabitler, sonra parametreleri saf veri olarak yerleştirir. Parametre içinde ne olursa olsun — bin tane OR 1=1 bile — asla komut olarak yorumlanamaz, çünkü ayrıştırma çoktan bitmiştir.


3. Mimarisel Bakış

Aşağıdaki diyagram, güvensiz bir sorguda kötü niyetli verinin sistemdeki yolculuğunu gösterir. güven sınırlarını işaret eder.

  Tarayıcı
   │  GET /product?id=5 OR 1=1
   ↓  ╳──────────── güven sınırı (dış) ────────────╳
  Apache / Nginx           (istek olduğu gibi geçer)
   ↓
  PHP-FPM Worker
   ↓
  Uygulama Kodu
   │   $sql = "... WHERE id = " . $_GET['id'];   ← komut+veri KARIŞIR
   ↓  ╳──────────── güven sınırı (DB) ────────────╳
  MySQL Ayrıştırıcı (Parser)
   │   "WHERE id = 5 OR 1=1"  → tümü tek komut sanılır
   │   Sorgu planı: TÜM satırları döndür
   ↓
  Storage / Tablolar
   │   Tüm users satırları okunur
   ↓
  Response  →  saldırgana tüm kullanıcılar döner

Güvenli akışta ise sınır, veritabanının içinde korunur:

  Uygulama Kodu
   │   prepare("... WHERE id = ?")   → yalnızca ŞABLON
   │   execute([$_GET['id']])        → yalnızca VERİ (ayrı kanal)
   ↓
  MySQL
   │   1) Şablonu ayrıştır, planı SABİTLE
   │   2) Parametreyi SAF VERİ olarak yerleştir
   │   "5 OR 1=1" → tek bir değer sanılır, id'si böyle bir ürün yok
   ↓
  Response → boş sonuç. Enjeksiyon etkisiz.

4. Güvensiz Kod

Aşağıdaki, gerçek projelerde sıkça rastlanan bir "ürün listeleme + arama + sıralama" uç noktasıdır. Yapaylıktan kaçınmak için birden fazla enjeksiyon bağlamı içerir.

<?php
// ⚠️ GÜVENSİZ — üretim benzeri ürün listeleme uç noktası
declare(strict_types=1);

$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'app', 'secret');

$category = $_GET['category'] ?? 'all';
$search   = $_GET['q'] ?? '';
$sortCol  = $_GET['sort'] ?? 'price';
$sortDir  = $_GET['dir'] ?? 'asc';
$page     = $_GET['page'] ?? '1';

$sql = "SELECT id, name, price, stock
        FROM products
        WHERE category = '$category'
          AND name LIKE '%$search%'
        ORDER BY $sortCol $sortDir
        LIMIT " . (($page - 1) * 20) . ", 20";

$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);

header('Content-Type: application/json');
echo json_encode($rows);

Bu kod çalışır. Testlerde "geçer". Kod incelemede aceleyle "PDO kullanıyor, iyi" diye onaylanabilir. Oysa dört ayrı enjeksiyon noktası barındırır.


5. Açığın Analizi

Kodu satır satır ve bağlam bağlam inceleyelim.

WHERE category = '$category' — Klasik dize bağlamı enjeksiyonu. $category tek tırnak içine alınmış. Saldırgan all' OR '1'='1 gibi bir değerle tırnaktan çıkıp koşulu değiştirebilir. En bilinen ve en çok öğretilen varyant budur.

name LIKE '%$search%' — Yine dize bağlamı, ancak LIKE içinde. Burada tırnak kaçışına ek olarak % ve _ joker karakterleri de bir sorun (LIKE injection); ayrıca tırnaktan çıkış hâlâ mümkündür.

ORDER BY $sortCol $sortDirKritik ve sık gözden kaçan nokta. $sortCol ve $sortDir tırnak içinde değildir. Bu yüzden hiçbir kaçış fonksiyonu (mysqli_real_escape_string, PDO quote) burada işe yaramaz — kaçış yalnızca dize/tırnak bağlamı içindir. Kolon adları ve sıralama yönü tanımlayıcı (identifier) bağlamındadır ve parametreli sorgularla da bağlanamaz. Bu, "PDO kullanıyorum ama hâlâ açığım" durumunun en yaygın nedenidir.

LIMIT ... , 20$page sayısal işleme sokulmuş gibi görünse de (($page - 1) * 20), PHP'nin gevşek tip davranışı burada aldatıcı olabilir; daha önemlisi, geliştiriciler LIMIT değerlerini de sıkça doğrudan gömer ve LIMIT bağlamı da bazı sürücülerde parametre bağlamayı kabul etmez.

Özet: Kırılma noktası her $ değişkeninin SQL metnine metin olarak gömülmesidir. Bağlam değiştikçe (dize / tanımlayıcı / sayısal) savunma da değişmek zorundadır — ve tek bir "kaçış" fonksiyonu bu bağlamların hepsini karşılamaz.


6. Hacker Bakış Açısı

Bir saldırgan bu açığı nasıl fark eder? Süreç sistematiktir ve gözlemseldir:

1. Girdi noktalarını haritalar. Uygulamadaki her parametreyi (URL, form, çerez, JSON alanı, hatta HTTP başlıkları) potansiyel giriş kabul eder. Sıralama, filtreleme, arama ve sayfalama parametreleri özellikle şüphelidir; çünkü bunlar sık sık doğrudan SQL'e gömülür.

2. Uygulamaya "gürültü" gönderir ve tepkiyi izler. Klasik ilk sonda tek bir tırnaktır ('). Uygulama beklenmedik bir davranış sergilerse — bir hata mesajı, boş bir sayfa, 500 durumu, farklı bir yanıt uzunluğu — bu, girdinin SQL ayrıştırıcısına ulaştığının ve onu bozduğunun işaretidir.

3. Sinyalleri sınıflandırır. Saldırgan üç tür geri bildirim arar: - Hata tabanlı sinyaller: Uygulama ayrıntılı SQL hatası döndürüyorsa (örn. You have an error in your SQL syntax), bu bir hediyedir — veritabanı tipi, sürüm, hatta sorgu yapısı sızar. - İçerik farkı: id=5 AND 1=1 ile id=5 AND 1=2 farklı sonuç döndürüyorsa, koşul enjekte edilebiliyor demektir (boolean-based blind). - Zamanlama farkı: Yanıt, enjekte edilen bir gecikme fonksiyonu (örn. bir SLEEP koşulu) ile ölçülebilir biçimde yavaşlıyorsa, hiç görünür çıktı olmadan bile açık doğrulanır (time-based blind).

4. Bağlamı belirler. Deneyimli bir gözlemci, tırnağın kırdığı yerden (dize mi, sayısal mı, ORDER BY mı) hangi bağlamda olduğunu çıkarır ve savunmanın nerede eksik olduğunu anlar.

Buradaki ders savunmacı içindir: Saldırganın en güçlü aracı sizin geri bildiriminizdir. Ayrıntılı SQL hataları üretimde asla dışarı sızmamalıdır; yanıtların gözlemlenebilir davranış farkları en aza indirilmelidir.


7. Exploit Mantığı

Bu bölüm bilinçli olarak kullanıma hazır saldırı dizeleri içermez. Amaç, bir açığın neden istismar edilebilir olduğunu ve araştırmacının onu nasıl analiz ettiğini kavratmaktır — bir hedefe zarar verme reçetesi vermek değil.

SQLi'nin istismar edilebilirliği tek bir kavramsal gerçeğe dayanır: Saldırgan, ayrıştırıcının sorgunun mantıksal yapısını değiştirmesini sağlayabildiği anda, sorgunun amacını yeniden yazabilir.

  • Mantık manipülasyonu: Bir WHERE koşuluna daima-doğru bir alt-ifade eklenebilirse, filtre etkisizleşir ve tüm kayıtlar döner. Kimlik doğrulama sorgusuysa, doğrulama atlanır.
  • Küme birleştirme: Ayrıştırıcı, orijinal sorgunun sonucuna ikinci bir SELECT'in sonucunu ekleyecek biçimde manipüle edilebilirse (kolon sayısı ve tipleri uyduğunda), saldırgan başka tablolardan veri çekebilir.
  • Kör çıkarım (blind inference): Görünür çıktı olmasa bile, saldırgan sorguya doğru/yanlış üreten koşullar enjekte edip yanıttaki ikili farkı (içerik veya süre) gözlemleyerek veriyi bit bit çıkarabilir. Bu, veriyi "sızdıran" değil, "tahmin edip doğrulayan" yavaş ama güvenilir bir yöntemdir.
  • Tırmanış: Veritabanı kullanıcısının yetkisi yüksekse (dosya okuma/yazma, xp_cmdshell benzeri işlevler, stored procedure), açık salt okumadan RCE'ye tırmanabilir. Bu, en az yetki ilkesinin neden bir savunma katmanı olduğunu gösterir.

Araştırmacının analiz çerçevesi şudur: (a) hangi bağlamda enjeksiyon var, (b) veritabanı ne tür geri bildirim veriyor, (c) kullanıcının yetkisi nereye kadar tırmanmaya izin veriyor. Savunmacı da tam bu üç ekseni kapatarak düşünmelidir.


8. Güvenli Kod

Aynı uç noktanın, her bağlamı doğru ele alan güvenli sürümü:

<?php
// ✅ GÜVENLİ — bağlam-duyarlı, parametreli, beyaz listeli
declare(strict_types=1);

$pdo = new PDO(
    'mysql:host=localhost;dbname=shop;charset=utf8mb4',
    'app_read',                       // en az yetkili kullanıcı (yalnızca SELECT)
    getenv('DB_PASSWORD'),
    [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // hataları yut ama fırlat
        PDO::ATTR_EMULATE_PREPARES   => false,   // GERÇEK prepared statement
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]
);

// 1) Değer bağlamları → parametre bağlama
$category = $_GET['category'] ?? 'all';
$search   = $_GET['q'] ?? '';

// 2) LIKE joker karakterlerini nötrle (LIKE injection'a karşı)
$searchEscaped = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $search);

// 3) Tanımlayıcı bağlamı → parametre BAĞLANAMAZ, BEYAZ LİSTE zorunlu
$allowedCols = ['price', 'name', 'stock', 'id'];
$sortCol = in_array($_GET['sort'] ?? '', $allowedCols, true)
    ? $_GET['sort']
    : 'price';                        // güvenli varsayılan

$sortDir = strtoupper($_GET['dir'] ?? '') === 'DESC' ? 'DESC' : 'ASC';

// 4) Sayısal bağlam → tip zorlaması + sınırlama
$page   = max(1, (int) ($_GET['page'] ?? 1));
$offset = ($page - 1) * 20;

$sql = "SELECT id, name, price, stock
        FROM products
        WHERE category = :category
          AND name LIKE :search
        ORDER BY {$sortCol} {$sortDir}   -- artık yalnızca beyaz-listeli sabit değerler
        LIMIT :offset, 20";

$stmt = $pdo->prepare($sql);
$stmt->bindValue(':category', $category, PDO::PARAM_STR);
$stmt->bindValue(':search', '%' . $searchEscaped . '%', PDO::PARAM_STR);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();

header('Content-Type: application/json');
echo json_encode($stmt->fetchAll());

Dikkat edilecek modern PHP araçları: - PDO::prepare + bindValue değer bağlamlarını komuttan ayırır. - ATTR_EMULATE_PREPARES => false kritiktir: true olduğunda PDO, parametreleri istemci tarafında dizeye gömerek taklit eder ve bazı kenar durumlarda (ör. charset istismarı) gerçek koruma sağlamaz. false, sorguyu sunucuya gerçekten iki kanaldan gönderir. - charset=utf8mb4 DSN'de belirtilir; bu, klasik "GBK charset" kaçış atlatma sınıfını kapatır. - Tanımlayıcılar için beyaz liste, parametre bağlamanın imkânsız olduğu tek doğru çözümdür.


9. Patch Analizi

Düzeltme neden işe yarar? Değer bağlamlarında parametreli sorgu, veritabanının ayrıştırma planını parametreler yerleştirilmeden önce sabitler. Parametre içindeki hiçbir meta-karakter komut sınırını değiştiremez, çünkü sınır çoktan çizilmiştir. Bu, "kaçış" gibi veriyi düzeltme değil, kanalları ayırma yaklaşımıdır ve bu yüzden eksiksizdir.

Alternatiflerin karşılaştırması:

Yaklaşım Değer bağlamı Tanımlayıcı (ORDER BY) Değerlendirme
Dize kaçışı (quote, real_escape_string) Kısmen Kenar durumlar ve tanımlayıcılar açık kalır. Tek başına yetersiz.
Parametreli sorgu (prepared) ✓ Tam ✗ (bağlanamaz) Değerler için altın standart.
Beyaz liste Tanımlayıcılar için tek doğru çözüm.
ORM / Query Builder ✓ (kolon adı doğrulamalı) Doğru kullanıldığında güvenli; raw() kaçış kapıları risklidir.

Performans etkisi: Gerçek prepared statement'lar, aynı sorgu tekrarlanırken sunucu tarafında planı yeniden kullanabildiği için genellikle nötr ya da olumludur. EMULATE_PREPARES => false her prepare için ek bir sunucu gidiş-dönüşü getirebilir; yüksek hacimli, tek seferlik sorgularda bu ölçülebilir ama küçüktür ve güvenlik kazancının yanında ihmal edilebilir. Beyaz liste kontrolü sabit zamanlıdır ve etkisi yok denecek kadar azdır.


10. Gerçek Hayat Senaryosu

Kurgu — bir orta ölçekli SaaS fatura platformu. Şirket, müşterilerine "raporları dışa aktar" özelliği sunar. Rapor ekranında bir "sırala" açılır menüsü vardır ve ön yüz, seçilen kolonu ?sort= parametresiyle backend'e gönderir. Geliştirici, açılır menüde yalnızca sabit seçenekler olduğu için ORDER BY $sort yazmanın "güvenli" olduğunu varsayar — çünkü arayüz yalnızca geçerli değerleri gönderir.

Sorun şudur: Güven sınırı arayüzde değil, sunucudadır. Bir müşteri, arayüzü atlayıp ?sort= parametresine doğrudan bir tanımlayıcı-bağlam enjeksiyonu gönderir. Veritabanı kullanıcısı — "kolaylık olsun" diye — tüm şemalarda okuma yetkisine sahiptir. Sonuç: bir müşteri, başka müşterilerin fatura verilerine ulaşır. Çok kiracılı (multi-tenant) bir sistemde bu, tek bir açıkla tüm kiracıların verisinin ifşasıdır — sözleşmesel, itibarî ve (KVKK/GDPR kapsamında) yasal olarak yıkıcı bir olay.

Ders: (1) Arayüz kısıtlaması bir güvenlik kontrolü değildir. (2) ORDER BY bağlamı, "PDO kullanıyorum" güvenlik yanılsamasının en sık kırıldığı yerdir. (3) En az yetki, açık gerçekleşse bile hasarı tek kiracıyla sınırlayabilirdi.


11. Gerçek CVE Analizi — CVE-2014-3704 (Drupageddon)

Aşağıdaki analiz, MITRE, NVD ve satıcı danışma belgeleriyle doğrulanmış bilgilere dayanır. Kavramsal mekanizma gösterilir; silahlaştırılmış istismar verilmez.

Özet. Drupal içerik yönetim sisteminin 7.x sürümlerinde, veritabanı soyutlama katmanındaki expandArguments fonksiyonu, hazırlanmış ifadeleri (prepared statements) doğru kurmadığı için, kimlik doğrulaması gerektirmeyen bir SQL Injection açığına yol açtı. Açık, SektionEins tarafından bildirildi ve 15 Ekim 2014'te SA-CORE-2014-005 danışma belgesiyle kamuoyuna açıklandı; Drupal 7.32 ile düzeltildi. Zafiyet CWE-89 (SQL Injection) sınıfındadır.

Teknik neden — ve neden bu kadar öğretici. İşin ironik yanı: Drupal parametreli sorgu kullanıyordu. Sistem, IN (...) gibi ifadeler için bir diziyi birden çok yer tutucuya (placeholder) genişleten expandArguments yardımcısına sahipti. Beklenen kullanım, dizinin değerlerinin parametre olarak bağlanmasıydı. Ancak fonksiyon, yer tutucu adlarını üretirken dizinin anahtarlarını (keys) güvenilir kabul ediyordu. Kavramsal olarak:

// Beklenen (güvenli) kullanım — dizi anahtarları sıralı sayılar:
$args = ['userids' => [1, 2, 3]];
// üretilen: WHERE id IN (:db_0, :db_1, :db_2)  → değerler bağlanır

// Beklenmeyen (istismar edilen) durum — saldırgan anahtarları kontrol edebilir:
// Anahtar, yer tutucu adının parçası olarak SORGU METNİNE gömülür.
// Böylece anahtar, saf veri değil, SQL yapısının bir parçası hâline gelir.

Drupal'ın giriş formu, dizi biçimli HTTP parametrelerini (name[...]) doğrudan bu mekanizmaya besliyordu. Saldırgan, HTTP parametre anahtarlarını özel biçimlendirerek, expandArguments'ın ürettiği sorgu metnine kendi SQL yapısını sokabiliyordu. Yani açık, "parametre bağlama kullanılmadığı için" değil, parametre bağlamanın bir bileşeni olan tanımlayıcı üretiminin güvenilmeyen girdiyle beslenmesi yüzünden oluştu. Bu, Bölüm 5'te vurguladığımız "değer bağlamı güvenli olsa bile tanımlayıcı bağlamı ayrı bir tehdittir" dersinin gerçek dünyadaki en net kanıtıdır.

Etki. Kimlik doğrulaması gerektirmeyen, uzaktan sömürülebilir bu açık, saldırganların yönetici hesabı oluşturmasına, keyfi SQL çalıştırmasına ve zincirleme olarak PHP kod çalıştırmasına imkân verdi. Yüz binlerce Drupal sitesi etkilendi; Drupal güvenlik ekibi, yamayı geç uygulayanların kısa süre içinde ele geçirilmiş sayılması gerektiği yönünde alışılmadık derecede sert bir uyarı yayımladı.

Satıcı düzeltmesi. Drupal 7.32, expandArguments fonksiyonunu, dizi anahtarlarını temizleyecek/normalize edecek biçimde yeniden yazdı; yamayı hemen uygulayamayanlar için ayrı bir patch dosyası dağıtıldı.

Çıkarılacak dersler: 1. "Parametreli sorgu kullanıyoruz" ifadesi, sorgunun her parçasının parametreleştiği anlamına gelmez. Tanımlayıcılar, yer tutucu adları ve şema öğeleri ayrı birer güven sınırıdır. 2. Bir soyutlama katmanı (framework/ORM), girdinin anahtarlarını güvenilir sayarsa, güvenli görünen bir API bile açık üretir. Kütüphane güvenliği, kullanıcının hem değerlerine hem anahtarlarına şüpheyle yaklaşmalıdır. 3. Yama hızı bir güvenlik kontrolüdür. Kamuya açıklanan, sömürüsü kolay bir açıkta saatler önemlidir.


12. Detection (Tespit)

Kod incelemede ne aranır? En verimli tarama, dize birleştirmeyle kurulan sorguları bulmaktır:

# SQL metnine gömülü değişken avı (kaba ama etkili ilk tarama)
grep -rnE '(query|exec|prepare)\s*\(.*\.\s*\$' src/
grep -rnE 'ORDER BY.*\$'                       src/   # tanımlayıcı bağlamı
grep -rnE '\$(_GET|_POST|_REQUEST).*(SELECT|INSERT|UPDATE|DELETE)' src/

İnceleyen kişi şunu sorar: Bu değişken bir değer mi, yoksa tanımlayıcı mı? Değerse parametre bağlanmış mı? Tanımlayıcıysa beyaz liste var mı?

SAST (statik analiz): SQLi tespitinde SAST araçları güçlüdür; çünkü açık, kaynaktan ($_GET) hedefe (query()) uzanan bir veri akışı (taint) izi olarak modellenebilir. Psalm/PHPStan'ın taint analizi, Semgrep kuralları veya ticari araçlar bu izleri bulur. Sınırları: dinamik olarak kurulan sorgular ve karmaşık soyutlama katmanları (Drupageddon'daki gibi) yanlış-negatif üretebilir.

DAST (dinamik analiz): Çalışan uygulamaya girdi göndererek davranış farkını gözlemler. Hata-tabanlı ve zaman-tabanlı sinyalleri iyi yakalar; ancak yalnızca eriştiği yüzeyi test edebildiği için kapsamı sınırlıdır.

WAF: Bilinen enjeksiyon kalıplarını (imza tabanlı) veya anomalileri yakalayabilir, ancak son savunma değil, ek katmandır. Kodlama, yorum varyasyonları ve bağlama özel yükler WAF'ı atlayabilir. WAF'a güvenip parametreli sorgu yazmamak temel bir hatadır.

Loglarda nasıl görünür / SIEM: Enjeksiyon denemeleri tipik izler bırakır: - Web sunucusu erişim loglarında ', --, UNION, SLEEP, %27 gibi kalıplar içeren parametreler. - Veritabanı loglarında sözdizimi hataları kümesi veya alışılmadık UNION/OR 1=1 sorguları. - Uygulama loglarında aynı IP'den kısa sürede çok sayıda 500 hatası veya yanıt-süresi anomalileri (time-based blind işareti). SIEM tarafında incelenmesi gereken korelasyon: tek bir kaynaktan, tek bir uç noktaya, artan karmaşıklıkta parametre denemeleri — bu, otomatik bir SQLi tarama imzasıdır.


13. Prevention (Önleme)

Açığın hiç oluşmaması için katmanlı yaklaşım:

Secure coding. Kesin kural: uygulama kodunda dize birleştirmeyle SQL kurulmaz. Değerler daima parametre bağlanır; tanımlayıcılar daima beyaz listeden geçer. Bu, tartışmaya açık bir stil tercihi değil, bir standarttır.

Secure design. Ham SQL'e erişimi merkezî bir veri katmanının arkasına gizleyin. Query builder / ORM kullanın; DB::raw() gibi kaçış kapılarını kod incelemede özel işaretleyin (bir "denylist" gibi izleyin). Böylece riskli yüzey tüm kod tabanına yayılmak yerine sayılı, denetlenebilir noktaya toplanır.

Threat modeling. Her yeni uç noktada "hangi girdiler veritabanına ulaşıyor ve hangi bağlamda (değer/tanımlayıcı)?" sorusunu STRIDE'ın "Tampering" ve "Information Disclosure" mercekleriyle sorun.

Code review. Değer bağlamı için "parametre var mı", tanımlayıcı bağlamı için "beyaz liste var mı" iki soruluk bir kontrolü zorunlu inceleme adımı yapın.

CI/CD & DevSecOps. SAST taramasını (taint kurallarıyla) ve bağımlılık taramasını pipeline'a gömün; kritik bulguda build'i kırın. Böylece güvensiz kalıp üretime ulaşamadan durur.

En az yetki. Uygulama DB kullanıcısına yalnızca gereken tabloların yalnızca gereken hakları verilir (çoğu uç nokta için sadece SELECT). Bu, açık gerçekleşse bile tırmanışı sınırlar.


14. Mitigation (Sistem Canlıyken)

Üretimde aktif bir SQLi açığıyla karşılaşıldığında, kalıcı düzeltme (parametreleştirme) beklenmeden atılacak adımlar:

Acil (dakikalar–saatler): - WAF'ta ilgili uç nokta/parametre için sanal yama (virtual patch) kuralı devreye alın; bu kesin çözüm değil, kanamayı durduran bir band-yardımdır. - İstismar edildiği doğrulanan uç noktayı gerekiyorsa geçici olarak devre dışı bırakın veya sıkı bir doğrulama (ör. yalnızca sayısal/beyaz-listeli değer) ekleyin. - Etkilenen DB kullanıcısının yetkilerini derhal daraltın (gereksiz FILE, yazma, çapraz-şema haklarını çekin).

Geçici (saatler–günler): - Erişim ve DB loglarını inceleyerek istismar kapsamını belirleyin: hangi veriye erişilmiş olabilir, yönetici hesabı oluşturulmuş mu. - Şüpheli oturumları/anahtarları iptal edin; sızmış olabilecek kimlik bilgilerini döndürün (rotate).

Kalıcı (günler): - Açık noktayı bu bölümdeki güvenli kod kalıbıyla yeniden yazın; parametreleştirme + beyaz liste. - Aynı kalıbı kod tabanının tamamında arayıp benzer noktaları toplu düzeltin (bir açık nadiren yalnızdır). - Olayı Incident Response sürecine bağlayın (Bölüm 32) ve gerekiyorsa veri ihlali bildirim yükümlülüklerini (KVKK/GDPR) değerlendirin.


15. Checklist

Kod inceleme: - [ ] Hiçbir sorgu dize birleştirmeyle kurulmuyor mu? - [ ] Tüm değer bağlamları parametre bağlanmış mı (prepare/bindValue)? - [ ] Tüm tanımlayıcı bağlamları (ORDER BY, kolon/tablo adı, LIMIT yönü) beyaz listeden mi geçiyor? - [ ] PDO::ATTR_EMULATE_PREPARES => false ayarlı mı? - [ ] DSN'de charset=utf8mb4 var mı? - [ ] LIKE sorgularında % ve _ joker karakterleri nötrleniyor mu? - [ ] Sayısal parametreler (int) ile zorlanıp aralık kontrolünden geçiyor mu?

Operasyon: - [ ] Uygulama DB kullanıcısı en az yetkili mi (gereksiz yazma/FILE/çapraz-şema yok)? - [ ] Üretimde ayrıntılı SQL hataları istemciye sızmıyor mu (display_errors=Off)? - [ ] SAST + bağımlılık taraması CI/CD'de zorunlu mu? - [ ] DB ve erişim logları SIEM'e akıyor ve SQLi kalıpları için alarm var mı?


16. Laboratuvar

Lab 6.1 — Enjeksiyonu kendi ortamınızda gözlemleyin. Yerel ve izole bir ortamda (Docker, dışa kapalı), bu bölümdeki güvensiz uç noktayı kurun. ?category=all' OR '1'='1 benzeri bir girdinin neden tüm satırları döndürdüğünü, ürettiği SQL'i loglayarak gözlemleyin. Sonra güvenli sürüme geçip aynı girdinin neden artık boş döndüğünü doğrulayın. (Yalnızca kendi izole ortamınızda.)

Lab 6.2 — Tanımlayıcı bağlamı tuzağı. Yalnızca ?sort= parametresini parametre bağlamaya çalışın (ORDER BY :col). Sorgunun neden beklendiği gibi çalışmadığını (veya sıralamanın neden hep aynı kaldığını) açıklayın. Ardından beyaz liste çözümünü uygulayın.

Lab 6.3 — EMULATE_PREPARES farkı. Aynı sorguyu ATTR_EMULATE_PREPARES true ve false iken çalıştırıp genel sorgu logunda (MySQL general log) ne göründüğünü karşılaştırın. false durumunda PREPARE/EXECUTE ayrımını gözlemleyin.


17. Quiz

  1. SQL Injection'ın tek cümlelik kök nedeni nedir?
  2. mysqli_real_escape_string neden ORDER BY $col bağlamında işe yaramaz?
  3. Parametreli sorgu, "kaçış"tan neden kavramsal olarak üstündür? Ayrıştırma sırasıyla açıklayın.
  4. PDO::ATTR_EMULATE_PREPARES => true ile false arasındaki güvenlik farkı nedir?
  5. Değer bağlamı ile tanımlayıcı bağlamı arasındaki fark nedir? Her biri için doğru savunma hangisidir?
  6. Boolean-based ve time-based "blind" SQLi arasındaki fark nedir? Hangi durumda hangisi gerekir?
  7. İkinci dereceden (second-order) SQLi nasıl oluşur? Bir senaryo verin.
  8. CVE-2014-3704 (Drupageddon), "parametreli sorgu kullanıyoruz" ifadesinin neden yeterli olmadığını nasıl kanıtlar?
  9. En az yetki ilkesi, SQLi önlenemediğinde hasarı nasıl sınırlar?
  10. WAF neden bir SQLi'ye karşı tek başına yeterli bir savunma değildir?
  11. LIKE injection nedir ve %/_ neden ayrıca ele alınmalıdır?
  12. SIEM tarafında bir SQLi tarama kampanyasını gösteren korelasyon kalıbı nedir?

18. Kaynakça

  • OWASP, SQL Injection Prevention Cheat Sheet.
  • OWASP, Top 10:2021 — A03 Injection.
  • OWASP, Testing for SQL Injection (WSTG).
  • MITRE, CWE-89: Improper Neutralization of Special Elements used in an SQL Command.
  • MITRE / NVD, CVE-2014-3704 kaydı.
  • Drupal Security Team, SA-CORE-2014-005: Drupal core — SQL injection.
  • SektionEins, Advisory 01/2014: Drupal — pre Auth SQL Injection Vulnerability.
  • PHP Manual, PDO Prepared Statements ve PDO::prepare referansları.
  • NIST NVD, CWE-89 sınıflandırması ve CVSS metrikleri.