SQL injection nedir
SQL injection (SQLi), kullanıcının verdiği metnin bir veritabanı sorgusuna yapıştırıldığı ve veritabanının sonunda onu SQL olarak çalıştırdığı bir zafiyettir. Uygulama bir e-posta adresi ya da ürün ID'si gibi bir değer göndermek ister; saldırgan bunun yerine sorgu dilinden bir parça gönderir ve sorgunun anlamı değişir.
Kök neden hep aynıdır: sorguyu string birleştirme ile kuran, iki tür veriyi tek bir string'de karıştıran kod. Sorgu metni sizin yazdığınız talimatlardır. Değer başkasının yazdığı veridir. İkisi birbirine yapıştırıldığında veritabanının sizin talimatlarınızın nerede bittiğini ve ziyaretçinin girdisinin nerede başladığını anlamasının hiçbir yolu kalmaz.
Bu yüzden SQL injection MySQL'e, PostgreSQL'e, SQL Server'a ya da SQLite'a özgü değildir, PHP'ye de özgü değildir. Bir sorgu güvenilmeyen string'lerden kurulduğu anda her dil, her sürücü ve her veritabanı açık hale gelir. CWE-89 olarak kataloglanmıştır ve enjeksiyon OWASP Top 10'un her sürümünde yer almıştır. Etrafındaki arama kümesi, "SQL saldırısı", "SQL açığı", "SQL injection saldırısı", hep bu tek hatayı anlatır.
İyi haber şu: çözüm de aynı ölçüde tektir, onlarca yıllıktır ve bugün kullanılan her veritabanı sürücüsünde yerleşiktir: sorguyu ve değerleri ayrı ayrı gönderin.
Nasıl çalışır: ders kitabı örneği
Sayısız eğitimin bir zamanlar yazdığı şekilde yazılmış bir giriş kontrolünü ele alalım. Kullanıcı adı ve parola bir formdan gelir ve doğrudan SQL string'inin içine bırakılır.
Normal girdiyle sorgu yapması gerekeni yapar. Şimdi bir ziyaretçinin parola alanına ' OR '1'='1 yazdığını düşünün. Tırnak, geliştiricinin açtığı string literal'ını kapatır ve geri kalanı WHERE koşulunun parçası olur. '1'='1' her zaman doğru olduğu ve AND, OR'dan daha sıkı bağlandığı için koşul her satırla eşleşir ve kod ziyaretçiyi tablodaki ilk kullanıcı olarak, ki bu çoğu zaman yöneticidir, içeri alır.
Kavramın tamamı o tek tırnaktır. Bu rehberdeki diğer her şey, farklı saldırı türleri, etki ve savunmalar, veritabanının tek bir string aldığı ve saldırganın metnini kod olarak ayrıştırdığı gerçeğinden çıkar.
(Parolaları düz metin olarak saklamak ve SQL içinde karşılaştırmak bu örnekteki ikinci hatadır. Gerçek kod kullanıcıyı adına göre getirir ve parolayı bir hash'e karşı password_verify() ile kontrol eder. Burada tutulmasının nedeni, enjeksiyonu göstermenin en kısa yolu olmasıdır.)
Açıklı: kullanıcı girdisi sorguya ekleniyor (PHP)
<?php
// DO NOT DO THIS
$user = $_POST['username'];
$pass = $_POST['password'];
$sql = "SELECT id FROM users
WHERE username = '$user' AND password = '$pass'";
$row = $pdo->query($sql)->fetch();
// With password ' OR '1'='1 the database receives:
// SELECT id FROM users
// WHERE username = 'admin' AND password = '' OR '1'='1'
// ...which is true for every row.
Çözüm: PHP ve Python'da parametreli sorgular
Parametreli sorgu (prepared statement ya da bağlı parametreler de denir), SQL metnini sürücüye göre ?, :name ya da %s gibi yer tutucularla gönderir ve değerleri ayrıca gönderir. Veritabanı önce sorguyu ayrıştırır ve planlar, sonra değerleri veri olarak yerine koyar. Bir değerdeki tırnak, string içindeki bir karakterden ibarettir; bir literal'ı kapatamaz ya da koşul ekleyemez, çünkü ayrıştırma çoktan bitmiştir.
Bu, bir durumu kaçırabilecek bir temizleme adımı değildir. Mekanizmayı tamamen ortadan kaldırır; her korunma listesinin en başında durmasının nedeni budur.
PHP'de yer tutucularla PDO ya da mysqli kullanın. PDO'da karakter setini DSN'de ayarlayın ve sürücünün gerçek sunucu tarafı prepared statement göndermesi için emulated prepare'i kapatın; hataların sessizce yok sayılmaması için exception'ları açın. Python'da her DB-API sürücüsü (sqlite3, psycopg, mysqlclient, PyMySQL) değerleri execute()'a ayrı bir argüman olarak alır. Python'daki tuzak, execute()'u çağırmadan önce string'i f-string ya da % ile kendiniz biçimlendirmektir: sonuç parametreli görünür ama birleştirmedir.
Aynı kural diğer her ortamda geçerlidir: Java'da PreparedStatement, .NET'te SqlParameter, node-postgres'te $1 yer tutucuları, Go'nun database/sql paketinde ?.
Güvenli: yer tutucular, değerler ayrı gönderiliyor (PHP PDO ve Python)
<?php
$pdo = new PDO('mysql:host=db;dbname=shop;charset=utf8mb4', $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE username = ?');
$stmt->execute([$_POST['username']]);
$row = $stmt->fetch();
$ok = $row && password_verify($_POST['password'], $row['password_hash']);
# Python (psycopg 3, PostgreSQL)
cur.execute(
"SELECT id, password_hash FROM users WHERE username = %s",
(username,), # values go here, never into the string
)
# WRONG, still injectable: the string is built before execute() sees it
cur.execute(f"SELECT id FROM users WHERE username = '{username}'")
SQL injection türleri
Güvenlik yazıları SQL injection'ı saldırganın veriyi nasıl geri aldığına göre sınıflandırır. Zafiyet her durumda aynıdır; farklı olan, uygulamanın neyi açığa vurduğudur.
In-band, UNION tabanlı. Enjekte edilen sorgunun sonucu sayfanın kendisinde geri gelir. Saldırgan bir UNION SELECT ekleyerek başka bir tablodan, örneğin kullanıcılar tablosundan satırları sayfanın zaten gösterdiği ürün listesine ekler. İstismar edilmesi en hızlı ve testte fark edilmesi en kolay türdür.
In-band, hata tabanlı. Uygulama veritabanı hata mesajlarını ekrana basar. Saldırgan, metni istediği veriyi, örneğin bir tablo adını ya da bir değeri içeren hatalar tetikler. Ziyaretçilere ham SQL hatalarını göstermek kör bir hatayı okunabilir hale getirir; hataları sunucu tarafında loglayıp kullanıcılara genel bir sayfa göstermek için iyi bir nedendir.
Kör (blind), boolean tabanlı. Sayfa ne veri ne hata gösterir, ama bir koşul doğru ya da yanlış olduğunda farklı davranır: bir ürün görünür ya da görünmez, bir sayfa 200 ya da 404 olur. Saldırgan evet-hayır sorularını tek tek sorarak veriyi parça parça okur. Yavaştır, ama araçlar bunu otomatikleştirir.
Kör, zaman tabanlı. Sayfa içeriği bile aynıdır; bu yüzden saldırgan bir koşul doğru olduğunda veritabanını bekletir ve yanıt süresini ölçer. Her yanıt aynı görünse de zamanlama yine bir kanaldır.
Out-of-band. Saldırgan veritabanı sunucusunun kendisinin, kontrol ettiği bir sisteme bağlanmasını sağlar; genellikle bir veritabanı fonksiyonunun tetiklediği bir DNS sorgusu ya da HTTP isteğiyle. Veritabanı özelliklerinin ve dışarıya ağ çıkışının mevcut olmasına bağlıdır; bu da bir veritabanı sunucusunun ihtiyacı olmayan dış bağlantıları açamaması için bir neden daha.
İkinci derece (stored). Girdi ilk seferde güvenli şekilde, doğru biçimde parametrelenerek saklanır, sonra daha sonra geri okunup farklı bir sorguya eklenir; çoğu zaman bir yönetici raporunda ya da bir arka plan işinde. Geliştiriciler kendi veritabanlarından gelen veriye güvenir; hata o güvendir. Savunma diğer her yerdekiyle aynıdır: kendi sakladığınız değerlerden kurulanlar dahil her sorguyu parametreleyin.
Saldırgan gerçekte ne elde eder
Etki, uygulamanın kullandığı veritabanı hesabının neye yetkisi olduğuyla sınırlıdır; bu rehberin ilerisinde en az yetkinin bu kadar önemli olmasının nedeni budur.
Veri okumak. Yaygın sonuç: müşteri kayıtları, e-posta adresleri, parola hash'leri, siparişler, ayar tablolarında saklanan API anahtarları. Yalnızca açıklı sorgunun dokunduğu tablo değil, o hesabın SELECT yapabildiği her tablo okunabilir.
Kimlik doğrulamayı atlatmak. Giriş örneğindeki gibi, belirli olması gereken bir koşul her zaman doğru hale gelir.
Veriyi değiştirmek ya da yok etmek. Hesap UPDATE, INSERT ya da DELETE yapabiliyorsa saldırgan da yapabilir: fiyatları değiştirmek, yönetici kullanıcılar oluşturmak, tabloları silmek. Bazı sürücü ve veritabanı kombinasyonları tek çağrıda birden fazla ifadeye izin verir; bu da kapsamı daha da genişletir.
Sunucuya ulaşmak. Güçlü yetkilerle bazı veritabanları sunucudaki dosyaları okuyabilir ya da yazabilir veya işletim sistemi komutları çalıştırabilir. Yönetici haklarına sahip bir veritabanı hesabında SQL injection, sunucunun tamamen ele geçirilmesine dönüşebilir.
Bu teorik değil. SQL injection, 100 milyonu çok aşan kartın verisinin çalındığı 2008 Heartland Payment Systems ihlalinin; Birleşik Krallık'ta düzenleyici para cezasıyla sonuçlanan 2015 TalkTalk ihlalinin; ve bir dosya transfer ürünündeki tek bir enjeksiyon açığının binlerce kuruma karşı kullanıldığı 2023 MOVEit Transfer toplu istismarının (CVE-2023-34362) giriş noktasıydı. Eski hata, güncel manşetler.
Önem sırasına göre korunma
1. Her yerde parametreli sorgular. Kendi veritabanınızdan, çerezlerden, başlıklardan ve iç servislerden gelen değerler dahil her sorgu, her değer. Yalnızca bu, zafiyeti kapatır. Diğer adımlar, birinin bir yerde unuttuğu durumda hasarı sınırlar.
2. ORM'inizi ya da query builder'ınızı doğru kullanın ve kaçış kapılarını bilin. Eloquent, Doctrine, Django ORM, SQLAlchemy, Hibernate ve Entity Framework sıradan sorguları sizin yerinize parametreler. Ham parçaları korumazlar: Laravel'de whereRaw, DB::raw, selectRaw ve orderByRaw, Django'da .extra() ve .raw(), SQLAlchemy'de text(), EF Core'da FromSqlRaw. Hepsi binding kabul eder; kullanın. Bu metot adlarını grep'lemek yapabileceğiniz en hızlı kod incelemesidir.
3. Parametre olamayanları izin listesine alın. Yer tutucular tanımlayıcıları değil değerleri tutar. Bir tablo adı, ORDER BY içindeki bir sütun ya da ASC/DESC kelimeleri bağlanamaz; bu yüzden bir "sıralama" parametresinin kodda sabit bir sütun adı listesine eşlenmesi gerekir. Asla olduğu gibi geçirmeyin.
4. Veritabanı kullanıcısı için en az yetki. Web uygulaması yalnızca kendi şemasında SELECT, INSERT, UPDATE ve DELETE yapabilen, başka hiçbir şey yapamayan bir kullanıcıyla bağlanmalıdır: DROP yok, GRANT yok, dosya erişimi yok, başka veritabanı yok ve asla root ya da sa hesabı değil. Migration'lar ayrı, daha güçlü bir kullanıcıyla çalışır. Bir enjeksiyon gözden kaçarsa, tek bir şemayı mı sızdıracağına yoksa sunucuyu mu ele geçireceğine bu karar verir.
5. Derinlemesine savunma olarak girdi doğrulama. Bir sipariş ID'si tamsayı olmalı, bir tarih tarih olarak ayrıştırılabilmeli, bir ülke kodu iki harf olmalıdır. Türleri ve biçimleri uygulamanızın girişinde doğrulamak, kötü niyetli girdinin büyük kısmını erkenden reddeder ve hataları yakalar. Çözüm değildir: bir ad alanı O'Brien'ı kabul etmek zorundadır ve bir yorum alanı neredeyse her şeyi kabul eder.
6. Hataları sızdırmayın. Veritabanı hatalarını sorgu ve bağlamla birlikte sunucu tarafında loglayın; ziyaretçiye genel bir hata sayfası gösterin. PHP'de bu, production'da display_errors=Off demektir.
Binding'li ham parçalar, izin listeli sıralama ve en az yetkili kullanıcı
// Laravel: raw expressions must carry their own bindings
$orders = DB::table('orders')
->whereRaw('total > ? AND status = ?', [$min, $status])
->get();
// ORDER BY cannot be bound: map input to known columns
$sortable = ['created_at', 'total', 'status'];
$column = in_array($request->sort, $sortable, true) ? $request->sort : 'created_at';
$dir = $request->dir === 'asc' ? 'asc' : 'desc';
$orders = Order::orderBy($column, $dir)->paginate(50);
-- MySQL: the app user gets data access to its own schema only
CREATE USER 'shop_app'@'10.0.%' IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_app'@'10.0.%';
Escape etmek neden yetmez
Prepared statement'lar yaygınlaşmadan önce öneri tırnakları escape etmekti: addslashes(), sonra mysql_real_escape_string(), sonra mysqli::real_escape_string(). Escape işlemi hâlâ pek çok kodda duruyor ve öngörülebilir şekillerde başarısız oluyor.
Yalnızca tırnaklı bağlamları korur. WHERE id = $id ifadesinde değerin etrafında tırnak yoktur, dolayısıyla escape edilecek bir şey de yoktur ve 1 OR 1=1 gibi bir girdi olduğu gibi geçer. Aynısı LIMIT, ORDER BY ve sayısal her şey için geçerlidir.
Karakter setine bağlıdır. Escape fonksiyonları bağlantının kodlamasıyla uyumlu olmak zorundadır. İstemci ve sunucu karakter seti arasındaki uyumsuzluklar, çok baytlı dizilerin escape eden ters eğik çizgiyi yutmasına izin vermiştir; addslashes() ise kodlamayı hiç bilmiyordu.
Her seferinde hatırlanması gerekir. Bir sorgudaki unutulmuş tek bir çağrı ya da SQL yerine HTML için escape edilmiş bir değer, ve açık geri gelir. Parametreli sorgular güvenli yolu varsayılan yol yapar.
Aynı mantık, SELECT ya da -- gibi kelimeleri silen kara listeler ve "temizleme" fonksiyonları için de geçerlidir. Saldırganlar yirmi yıldır bu tür filtrelerin arasından sızan kodlamalar, yorumlar ve büyük-küçük harf varyasyonları buluyor. Kendi yazdığınız her filtreyi bir kolaylık olarak görün, asla koruma olarak değil.
Kendi uygulamanızı güvenle test etmek
Saldırıyla değil, kodla başlayın. SQL injection'ın çoğu bir kod aramasında görünür: ., +, string interpolation ya da sprintf ile kurulan sorgular ve bir değişkenle çağrılan ORM raw metotları. Semgrep ve CodeQL gibi statik analiz araçları tam olarak bu kalıp için kurallarla gelir ve her pull request'te CI içinde çalışabilir.
Sonra formlarınızdan ve API'lerinizden düşmanca görünen ama zararsız değerler, tek bir tırnak, O'Brien gibi bir ad, çok uzun bir string, geçiren testler yazın ve uygulamanın normal bir sonuç ya da doğrulama hatası döndürdüğünü, asla bir 500 ve asla bir veritabanı hata mesajı döndürmediğini doğrulayın. Bu testler test setinde kalır ve gerilemelere karşı korur.
Dinamik test için OWASP ZAP ve sqlmap gibi açık kaynak tarayıcılar, çalışan uygulamaları enjekte edilebilir parametreler için yoklar. Özellikle sqlmap bir istismar aracıdır: bir açık bulduğunda veri çeker. Onu yalnızca sahibi olduğunuz ya da test etmek için yazılı izniniz olan sistemlere karşı çalıştırın, tercihen sahte veri içeren bir staging kopyasında; çünkü yoklamaları loglara yazar, veriyi değiştirebilir ve izleme sisteminizi tetikleyebilir. Başkasının sitesini izinsiz taramak, niyet ne olursa olsun çoğu ülkede yasa dışıdır.
CDN'iniz üzerinden test ediyorsanız WAF'ın yoklamaların çoğunu engellemesini bekleyin. Bu WAF'ın işini yapmasıdır, ama uygulamanın gerçek davranışını da gizler; bu yüzden uygulamanın kendisini staging'de test edin ve WAF'ı ayrı bir katman olarak ele alın.
# PHP: query calls that mention a variable (review each hit)
grep -rnE '(query|exec|prepare)\(.*\$[A-Za-z_]' --include='*.php' app/
# Laravel / Eloquent raw fragments: check each one has bindings
grep -rnE '(whereRaw|selectRaw|orderByRaw|havingRaw|DB::raw)\(' app/
# Python: f-strings or % formatting inside execute()
grep -rnE "execute\(\s*f['\"]|execute\(.*['\"]\s*%\s" --include='*.py' .
WAF'ın yeri: bir katman, çözüm değil
Bir web uygulama güvenlik duvarı (WAF), her isteği uygulamanıza ulaşmadan önce inceler ve saldırıya benzeyenleri engeller. SQL injection query string'lerde ve form gövdelerinde tanınabilir izler bırakır; bu yüzden bu, WAF'ın iyi olduğu işlerden biridir: internetteki her siteyi yoklayan otomatik tarayıcıları durdurur ve açıklı bir eklenti siz yamalayamadan duyurulduğunda zaman kazandırır.
Yine de kodunuzda yaşayan bir zafiyete karşı yapılan bir kalıp eşleştirmedir. Hedefli bir saldırgan payload'ı kalıplardan kaçacak şekilde biçimlendirir ve bazı enjeksiyon noktaları, alışılmadık bir kodlamadaki JSON alanları, bir sorguya arka plan işi üzerinden ulaşan değerler ya da ikinci derece enjeksiyon gibi, bir istekte hiç şüpheli görünmez. OWASP Top 10 rehberi, bir WAF'ın neyi kapsayıp neyi kapsayamadığını çıkarıyor. Çözüm parametreli sorgular olarak kalır.
İşletme maliyeti false positive'lerdir. Enjeksiyonu yakalayacak kadar sıkı her kural bazen bir insanı engeller: kod örneği içeren bir yazıyı kaydeden bir editör, birinin SQL içeren bir hata mesajını yapıştırdığı bir destek formu. Olağan uygulama, yeni kuralları önce tespit (yalnızca log) modunda çalıştırmak, neyin engelleneceğini okumak ve sonra uygulamaya almaktır; bir alanın meşru olarak SQL'e benzer metin taşıdığı yerlerde dar istisnalarla.
CDN.com.tr'de WAF, Yayınlama Kuralları sayfasından hesap bazında açılan OWASP Core Rule Set ile ModSecurity'dir. Bu kural setinde 942 ailesi SQL injection kurallarıdır. Engellenen ziyaretçi, denetim logundaki istek tanımlayıcısı olan bir referans ID içeren markalı bir 403 sayfası alır; böylece tam olarak hangi kuralın hangi alanda eşleştiğini görebilirsiniz. Paneldeki WAF log sayfası engellenen olayları saldırı kategorisi, ülke, IP ve kuralla listeler, CSV ya da XLSX olarak dışa aktarır; cdnctl waf logs aynısını komut satırından gösterir. Meşru bir alan bir kurala takıldığında çözüm dar olmalıdır: site için korumayı kapatmak yerine o kural, o alanda, o yolda. Tek yollar için istisnalar henüz panelde yok; isteğin referans ID'sini desteğe gönderin. Giriş formları için rota bazında rate limiting aynı sayfadadır ve çoğu zaman enjeksiyon taramalarıyla birlikte gelen brute-force trafiğini yavaşlatır.
SQL injection hakkında sık sorulanlar
SQL injection 2026'da hâlâ bir sorun mu?
Evet. Modern framework'ler güvenli yolu varsayılan yapıyor, ama ham sorgular, eski kod, eklentiler ve hızlıca yazılmış iç araçlar hâlâ string birleştiriyor; yaygın kullanılan ürünlerdeki yeni enjeksiyon açıkları duyurulmaya ve büyük ölçekte istismar edilmeye devam ediyor. Enjeksiyon OWASP Top 10'un her sürümünde yer almıştır.
Prepared statement'lar her SQL injection'ı önler mi?
Bağladığınız her değer üzerinden yapılan enjeksiyonu önler. Tablo ya da sütun adları veya sıralama yönü gibi tanımlayıcıları bağlayamazlar ve önce bir string kurup sonucu "prepare" ederseniz işe yaramazlar. Tanımlayıcıları izin listesine alın ve girdiyi asla SQL metnine biçimlendirmeyin.
ORM kullanmak beni SQL injection'dan korur mu?
Sıradan sorgularda büyük ölçüde evet. Her ORM'in SQL'i değiştirmeden geçiren whereRaw, DB::raw, .raw(), text() ya da FromSqlRaw gibi raw metotları vardır. Onları binding'lerle kullanın ve her çağrıyı inceleyin.
Girdi doğrulama SQL injection'ı durdurmaya yeter mi?
Hayır. Doğrulama faydalı bir ikinci hattır: bir ID sayısal olmalı, bir tarih ayrıştırılabilmelidir. Ama birçok alan tırnak ve serbest metin kabul etmek zorundadır; bu yüzden doğrulama koruma olamaz. Koruma parametreli sorgulardır.
WAF SQL injection'ı tamamen durdurabilir mi?
Otomatik denemelerin çoğunu durdurur ve hedefli saldırılar için gereken çabayı artırır, ama isteklerdeki kalıpları eşleştirir ve kararlı bir saldırgan payload'ı bunlardan kaçacak şekilde uyarlayabilir. İkinci derece enjeksiyon bir istekte hiç şüpheli görünmez. WAF'ı derinlik için, parametreli sorguları çözüm olarak kullanın.
Bir web sitesini sqlmap ile SQL injection için test etmek yasal mı?
Yalnızca sahibi olduğunuz ya da test etmek için açık yazılı izniniz olan sistemlerde. Başkasının sitesini taramak çoğu hukuk düzeninde yetkisiz erişimdir. Kendi staging ortamınızı sahte veriyle test edin; başkasının ürününde bir açık bulursanız, onların bildirim süreci üzerinden raporlayın.
SQL injection ile XSS arasındaki fark nedir?
SQL injection veritabanınıza saldırganın yazdığı SQL'i çalıştırtır. Cross-site scripting ise bir ziyaretçinin tarayıcısına saldırganın yazdığı JavaScript'i sitenizin bağlamında çalıştırtır. İkisi de enjeksiyondur, kök nedenleri aynıdır: veriyle kodu karıştırmak; ve her birinin kendi çözümü vardır: SQL için parametreli sorgular, HTML için bağlama duyarlı çıktı kodlaması. Bkz. XSS nedir?.