İçeriğe geç
academia.sh

Ders 11 / 15

SQL Enjeksiyonu Önleme

Sorgu metnini istemcinin değerinden ayıran parametreli sorguyu ölçmek: aynı sorgu birleştirmede değer değişince değişir, hazır deyimde değişmez, ve bağlama her yerde çalışmaz.

Yayın Güncelleme

Bu derste

  • Birleştirilmiş sorgu metni ile parametreli sorgu metnini karşılaştırır ve hangisinin istemciye bağlı olduğunu ölçer
  • Hazır deyim ve bağlama ile değeri sorgu metninden ayırır
  • ORDER BY gibi bağlamanın geçerli olmadığı yerde izin listesi kullanır
İçindekiler

Girdi Doğrulama ve Arındırma dersinde doğrulanmış adedi yine istemci belirliyordu; doğrulama yalnız geçerli kümeyi daralttı. Aynı değer bir sorgu metninin içine girerse soru değişir: sorgunun metnini kim belirliyor? Metin istemcinin değerinden oluşuyorsa, sorgunun ne yapacağı da ondan oluşur.

GÜ2. Bu derste sorguya giren değer nötr bir addır: ayse ve mehmet. İki ad da geçerli kullanıcı adıdır; ölçülen şey, adın değişmesiyle sorgu metninin değişip değişmediğidir. Sorgu metnini bozacak girdiler yazılmaz ve denenmez. Bütün sorgular bellekteki veritabanında koşar.

Ortak Veri

Ölçümler aynı küçük tabloyu kullanır: üç kullanıcı, kasıtlı olarak alfabe dışı sırayla eklenmiş. Dosya yalnız diske yazılır.

<?php
// veri.php — bellekteki veritabanı ve üç kullanıcı
declare(strict_types=1);

function baglan(): PDO {
    $pdo = new PDO('sqlite::memory:');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->exec('CREATE TABLE kullanicilar (id INTEGER PRIMARY KEY, ad TEXT)');
    foreach (['mehmet', 'zeynep', 'ayse'] as $ad) {
        $pdo->prepare('INSERT INTO kullanicilar (ad) VALUES (?)')->execute([$ad]);
    }
    return $pdo;
}

Birleştirme ve Parametreli Sorgu

Birleştirilmiş sorguda değer, metnin parçası olur. Parametreli sorguda (prepared statement) metin sabittir ve değer ayrı bir adımda bağlanır (binding): prepare() önce yalnızca yer tutuculu metni hazırlar, execute() değeri o metne yerleştirmeden, ayrı iletir. Hazır deyimin elindeki metne queryString özelliğinden bakılabilir; aşağıdaki ölçüm iki yöntemin sorgu metnini iki adla karşılaştırır.

<?php
// sorgu.php — iki yöntemin sorgu metni
declare(strict_types=1);
require 'veri.php';

function birlestirmeMetni(string $ad): string {
    return "SELECT id, ad FROM kullanicilar WHERE ad = '" . $ad . "'";
}

function parametreliMetin(PDO $pdo, string $ad): string {
    $deyim = $pdo->prepare('SELECT id, ad FROM kullanicilar WHERE ad = ?');
    $deyim->execute([$ad]);
    return $deyim->queryString;
}

Dosya yalnız fonksiyonları tanımlar. Ölçüm, iki adla iki yöntemi yan yana çağırır.

<?php
// karsilastir.php — iki yöntemde sorgu metni
declare(strict_types=1);
require 'sorgu.php';

$pdo = baglan();
foreach (['ayse', 'mehmet'] as $ad) {
    echo 'birleştirme  ', birlestirmeMetni($ad), "\n";
    echo 'parametreli  ', parametreliMetin($pdo, $ad), "\n";
}
birleştirme  SELECT id, ad FROM kullanicilar WHERE ad = 'ayse'
parametreli  SELECT id, ad FROM kullanicilar WHERE ad = ?
birleştirme  SELECT id, ad FROM kullanicilar WHERE ad = 'mehmet'
parametreli  SELECT id, ad FROM kullanicilar WHERE ad = ?

Birleştirmede ad değişince sorgu metninin kendisi değişti: 'ayse' yerine 'mehmet' metne yazıldı. Parametreli sorguda metin iki adda da aynı kaldı; ad metnin dışında, bağlanan değer olarak taşındı. Bu fark güvenlik özelliğinin tamamıdır: metnin değeri istemciden alınmış olması, metnin yapısını istemcinin değiştirebileceği anlamına gelir. Metin sabit olduğunda değer ne olursa olsun yalnız değer olarak kalır.

Taraf: Metni Kim Belirliyor

Aynı ölçümü kursun sorusuna bağlayalım. Çekirdek dosya, her derste olduğu gibi taraf.php’dir; sina() sonucu sapmasız hesaplar, sonra her dış tarafın sapmasıyla yeniden hesaplar ve taraf() değiştiren ilk tarafı seçer. Karar sırası istemci, üçüncü taraf, ortam; hiçbiri değiştiremediyse sunucu.

<?php
// taraf.php — bu sonucu kim değiştirebiliyor?
declare(strict_types=1);

// Kodun dışındaki taraflar, karar sırasıyla: her istek, her dış çağrı, her dağıtım.
const DIS_TARAFLAR = ['istemci', 'üçüncü taraf', 'ortam'];

/** Sonucu bir kez sapmasız, sonra her dış tarafın sapmasıyla ayrı ayrı hesaplar. */
function sina(callable $sonuc, array $sapmalar): array {
    $taban = $sonuc([]);
    $degisti = [];
    foreach (DIS_TARAFLAR as $taraf) {
        if (array_key_exists($taraf, $sapmalar)) {
            $degisti[$taraf] = $sonuc([$taraf => $sapmalar[$taraf]]) !== $taban;
        }
    }
    return $degisti;
}

/** Sonucu değiştirebilen ilk dış taraf; hiçbiri değiştiremiyorsa sunucu. */
function taraf(array $degisti): string {
    foreach (DIS_TARAFLAR as $t) {
        if ($degisti[$t] ?? false) return $t;
    }
    return 'sunucu';
}

function tarafRaporu(string $ad, callable $sonuc, array $sapmalar): string {
    $degisti = sina($sonuc, $sapmalar);
    $satirlar = [];
    foreach ($degisti as $t => $d) $satirlar[] = $t . ($d ? ' değiştirdi' : ' değiştiremedi');
    $sonTaraf = taraf($degisti);
    echo "$ad → $sonTaraf\n    ", implode(' · ', $satirlar), "\n";
    return $sonTaraf;
}

Sonuç, prepare()’e giden sorgu metnidir. İstemcinin sapması ayse yerine mehmet göndermektir.

<?php
// enjeksiyon.php — sorgu metninin tarafı
declare(strict_types=1);
require 'taraf.php';
require 'sorgu.php';

$pdo = baglan();
tarafRaporu('birleştirme', fn(array $s) => birlestirmeMetni($s['istemci'] ?? 'ayse'), ['istemci' => 'mehmet']);
tarafRaporu('parametreli', fn(array $s) => parametreliMetin($pdo, $s['istemci'] ?? 'ayse'), ['istemci' => 'mehmet']);
birleştirme → istemci
    istemci değiştirdi
parametreli → sunucu
    istemci değiştiremedi

Birleştirmede sonucu istemci değiştirdi. Parametreli sorguda metni değiştirebilecek üç taraf da değiştiremedi; kalan sahip sunucudur, çünkü metin kodda sabit yazılmıştır. Burada kaçışlama ya da doğrulama gibi bir daralma yoktur: sorgu metninin sahibi tümüyle el değiştirmiştir. Doğrulama değerin kümesini daraltır ve istemci kalır; hazır deyim ise metni istemcinin erişemeyeceği bir yere koyar.

Sınırlayıcı Ölçüm

Bağlama her yerde çalışmaz: yer tutucu yalnız değerin yerine konur; sütun adı, tablo adı ya da sıralama yönü gibi sorgu yapısının parçaları bağlanamaz. ORDER BY ? yazıp sütun adını bağlayan kişi, sıralamayı bağlamış olmaz; sayı ya da dizge sabiti bağlamış olur. Aşağıda aynı kullanicilar tablosu ad sütununa göre sıralanmak istenir; bağlama ile sıralama istenen sütunun adı verilir.

<?php
// siralama.php — bağlama ve izin listesi
declare(strict_types=1);
require 'veri.php';

const SIRALAMA_IZINLERI = ['ad' => 'ad', 'id' => 'id'];

function baglamaIleSirala(PDO $pdo, string $sutun): array {
    $deyim = $pdo->prepare('SELECT ad FROM kullanicilar ORDER BY ?');
    $deyim->execute([$sutun]);
    return $deyim->fetchAll(PDO::FETCH_COLUMN);
}

function izinListesiyleSirala(PDO $pdo, string $istek): array {
    $sutun = SIRALAMA_IZINLERI[$istek] ?? 'id';
    return $pdo->query('SELECT ad FROM kullanicilar ORDER BY ' . $sutun)->fetchAll(PDO::FETCH_COLUMN);
}

$pdo = baglan();
echo 'bağlama      ', implode(', ', baglamaIleSirala($pdo, 'ad')), "\n";
echo 'izin listesi ', implode(', ', izinListesiyleSirala($pdo, 'ad')), "\n";
echo 'listede yok  ', implode(', ', izinListesiyleSirala($pdo, 'sifre')), "\n";
bağlama      mehmet, zeynep, ayse
izin listesi ayse, mehmet, zeynep
listede yok  mehmet, zeynep, ayse

Bağlamayla sıralama beklenen sonucu vermedi: satırlar ad sırasıyla değil, eklenme sırasıyla geldi (mehmet, zeynep, ayse). Bağlanan 'ad' bir sütun adı olarak değil, her satırda aynı olan bir dizge değeri olarak okundu ve sıralama bir şey değiştirmedi. Sorgu hata da vermedi; yanlışlık sessizdir. İzin listesi sıralamayı gerçekten uyguladı (ayse, mehmet, zeynep) ve listede olmayan sifre isteği hataya düşmeden varsayılan id sırasına döndü. Sütun adı sorguya girerse de girdiği yer kodda sabit yazılı bir listedir; istemcinin değeri yalnız listeden bir anahtar seçer.

Özet

  • Bu dersin ölçü sınıfı birleştirmede istemci, parametreli sorguda sunucu: sorgu metnini ikincisinde yalnızca prepare()’e verilen sabit dizge belirler.
  • Birleştirmede adı ayse’den mehmet’e değiştirmek sorgu metnini değiştirdi; parametreli sorguda metin iki adda da aynı kaldı.
  • Hazır deyim ve bağlama değeri metinden ayırır; savunma sonucu sahibinden alır, daraltmakla yetinmez.
  • Bağlama yalnız değer içindir: ORDER BY ? sıralamayı değiştirmedi ve sütun adı bağlanamadı.
  • Sorgu yapısının parçası istemciden geliyorsa çözüm, kod içinde sabit bir izin listesinden seçmektir; listede olmayan istek varsayılana düşer.

Sonraki Adım

Sorgu metnini sunucuya almak bir yöndeki yolu kapattı. Aynı değer bu kez bir sayfaya basılırsa, çıktının metnini kim belirler? Siteler Arası Betik Çalıştırma Önleme dersi, çıktı anında kaçışlamayı aynı taraf sorusuyla ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat