SQL Injection Nedir ve Nasıl Önlenir? Güvenli SQL Rehberi
SQL injection saldırıları nasıl çalışır? Parametreli sorgular, ORM kullanımı, girdi doğrulama ve veri güvenliği yöntemleriyle SQL açıklarını kapatma rehberi.
- SQL injection (SQLi), kullanıcı girdilerinin SQL komut yapısını bozarak yetkisiz sorgu çalıştırılmasına yol açan bir güvenlik açığıdır.
- Dinamik metin birleştirme yerine hazırlıklı ifadeler (prepared statements) ve parametreli sorgular kullanmak açığı kesin olarak engeller.
- ORM kütüphaneleri sorguları varsayılan olarak parametrelendirse de ham SQL metin birleştirmelerinde açık tekrar ortaya çıkar.
- Veritabanı rollerine en az yetki (least privilege) kuralı uygulanmalı ve uygulama bağlantısına yönetimsel DDL yetkileri verilmemelidir.
SQL injection (SQLi), web uygulamalarında en sık rastlanan ve doğrudan veri sızıntısına yol açan güvenlik açıkları arasında ilk sıralarda yer alır. Temel mekanizma, istemciden gelen güvenilmeyen girdilerin veritabanı motoruna gönderilen SQL komut metnine filtrelenmeden eklenmesidir. Veritabanı ayrıştırıcısı (parser), geliştiricinin veri olarak planladığı metni SQL komut parçası olarak yorumlar ve sorgunun yürütme mantığını saldırganın istediği yönde değiştirir.
Güvenlik açıklarının büyük çoğunluğu karmaşık şifreleme kırılmalarından değil, basit string birleştirme hatalarından doğar. Tek bir form alanından veya API parametresinden sızan tırnak işareti, tüm veritabanı tablolarının okunmasına, değiştirilmesine veya silinmesine zemin hazırlar.
SQL Injection Nasıl Ortaya Çıkar?
Veritabanı motoru bir SQL metnini aldığında önce sözdizimsel analiz (lexical parsing) yapar ve bir soyut sözdizim ağacı (AST) oluşturur. Geliştirici kullanıcı girdisini string birleştirme yoluyla sorguya dahil ettiğinde, girdi ayrıştırıcı aşamasında komutun sözdizimini bozar.
Örnek bir kimlik doğrulama senaryosunda güvensiz sorgu yapısı şöyle görünür:
// Güvensiz uygulama kodu
const query = `SELECT id, role, email FROM users WHERE username = '${username}' AND password = '${passwordHash}'`;
const result = await db.query(query);
Saldırgan username alanına admin' -- değerini gönderdiğinde oluşan nihai SQL sorgusu şu hali alır:
SELECT id, role, email FROM users WHERE username = 'admin' --' AND password = 'hash';
SQL standartlarında çift tire (--) karakterinden sonraki tüm metin yorum satırı sayılır. Veritabanı şifre kontrolünü tamamen yok sayarak admin kullanıcısının bilgilerini döner. Saldırgan şifreyi bilmeden oturum açar.
Benzer şekilde username alanına ' OR '1'='1 gönderildiğinde WHERE koşulu her zaman doğru (true) sonucunu verir. Sorgu tablodaki ilk kullanıcıyı döndürür ve kimlik doğrulama katmanı aşılır.
Temiz ve hatasız sorgu şablonları oluştururken sözdizim yapısını doğru kurgulamak için SQL sorgu formatlayıcı aracından yararlanabilir, karmaşık sorguları standartlaştırmak için SQL düzenleme rehberi içeriğini inceleyebilirsiniz.
SQLi Saldırı Türleri ve Tespit Yöntemleri
SQL injection saldırıları veritabanından veri çıkarma yöntemine ve dönen yanıt yapısına göre üç ana kategoriye ayrılır:
| Saldırı Türü | Temel Mekanizma | Tipik Kullanım Alanı | Tespit Zorluğu |
|---|---|---|---|
| In-Band (Klasik / Union-Based) | Yanıt doğrudan HTTP çıktısı veya hata mesajı içinde döner | Hızlı veri çekme, tablo adlarını listeleme | Düşük |
| Error-Based (Hata Tabanlı) | Veritabanı hata mesajları içine veri gömülür | Veri yapısını ve veritabanı sürümünü öğrenme | Düşük |
| Blind (Kör / Boolean-Based) | Sayfadaki içerik değişiminden true/false çıkarımı yapılır | Hata vermeyen kapalı sistemlerde veri sızdırma | Orta |
| Time-Based (Zamana Dayalı) | pg_sleep() veya WAITFOR DELAY ile gecikme ölçülür | Hiçbir çıktı vermeyen arayüzlerde karakter analizi | Yüksek |
| Out-of-Band (Kanal Dışı) | DNS sorgusu veya HTTP isteğiyle veri dış sunucuya taşınır | Doğrudan yanıt alınamayan asenkron sorgular | Yüksek |
In-band saldırılarda en popüler teknik UNION SELECT operatörüdür. Orijinal sorgunun kolon sayısı ile tip uyumu yakalandığında, saldırgan başka tablolardaki gizli alanları (örneğin credit_cards veya tokens) mevcut yanıt listesine ekler.
Kör (blind) saldırılarda ise ekranda veri ya da hata görünmez. Saldırgan veritabanına SUBSTRING(password, 1, 1) = 'a' gibi sorular sorar. Yanıt doğruysa sayfa normal yüklenir; yanlışsa farklı bir içerik döner veya pg_sleep(5) komutuyla sayfa 5 saniye geç açılır. Bu yöntemle parolanın her bir harfi ikili arama (binary search) mantığıyla saniyeler içinde çözülür.
Hazırlıklı İfadeler ve Parametreli Sorgular
SQL injection açığına karşı en etkili ve kesin savunma mekanizması hazırlıklı ifadelerdir (prepared statements). Bu yaklaşım, sorgunun SQL derleme aşaması ile veri iletim aşamasını kesin çizgilerle birbirinden ayırır.
Veritabanı motoru sorguyu önce bir şablon olarak alır, sözdizimini ayrıştırır ve sorgu yürütme planını (execution plan) hazırlar. Kullanıcı girdisi bu şablona veri tipleri belirlenmiş parametreler ($1, $2 veya ?) olarak iletilir. Girdi içinde tırnak, noktalı virgül veya SQL komutları olsa bile motor bu girdiyi hiçbir zaman yürütülebilir kod olarak işlemez; yalnızca düz metin verisi olarak ele alır.
Node.js ortamında pg kütüphanesi ile güvenli sorgu örneği:
import { Client } from 'pg';
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
// Güvenli parametreli sorgu
const query = 'SELECT id, role, email FROM users WHERE username = $1 AND is_active = $2';
const values = [userInputUsername, true];
const res = await client.query(query, values);
Python psycopg2 ile güvenli sorgu örneği:
import psycopg2
conn = psycopg2.connect(dsn)
cursor = conn.cursor()
# Parametreli sorgu: Girdi tuple olarak ayrı iletilir
sql = "SELECT id, email FROM users WHERE username = %s AND status = %s"
cursor.execute(sql, (user_input, "active"))
rows = cursor.fetchall()
Her iki örnekte de sürücü, veriyi SQL derleyicisine değil, derlenmiş sorgunun veri yuvasına gönderir. Kullanıcı girdisi ne kadar zararlı karakter içerirse içersin SQL injection oluşamaz.
ORM ve Query Builder Kullanımında Yapılan Hatalar
Prisma, TypeORM, Drizzle, Sequelize veya SQLAlchemy gibi modern ORM kütüphaneleri, standart metodlarında (örneğin findUnique, where, select) sorguları otomatik olarak parametrelendirir. Bu sayede standart ORM çağrılarında SQL injection riski bulunmaz.
Ancak geliştiriciler karmaşık filtreleme, raporlama veya dinamik sıralama yaparken ORM'lerin sunduğu ham SQL fonksiyonlarına yönelir. Açıklar tam olarak bu noktalarda meydana gelir:
// Prisma kullanırken yapılan tehlikeli hata:
// $queryRawUnsafe metnine doğrudan string ekleme
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE email = '${userEmail}'`
);
// Doğru kullanım: Prisma template tag ($queryRaw) parametreleri otomatik ayırır
const safeUsers = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email = ${userEmail}
`;
Dinamik kolon adı veya sıralama yönü (ORDER BY) belirlenirken veritabanı sürücüleri parametre bağlamaya izin vermez. Kolon adları SQL sözdiziminin parçasıdır. Bu durumlarda kullanıcı girdisi asla doğrudan sorguya eklenmemelidir. Girdinin izin verilen bir beyaz listede (allowlist) bulunup bulunmadığı kontrol edilmelidir:
const ALLOWED_SORT_COLUMNS = ["created_at", "username", "email"] as const;
type SortColumn = typeof ALLOWED_SORT_COLUMNS[number];
function getSafeSortColumn(input: string): SortColumn {
if (ALLOWED_SORT_COLUMNS.includes(input as SortColumn)) {
return input as SortColumn;
}
return "created_at"; // Güvenli varsayılan değer
}
Çok Katmanlı Güvenlik (Defense-in-Depth)
Parametreli sorgular birincil kalkan olsa da savunmayı tek bir önleme bırakmamak gerekir. Sistem mimarisinde uygulanması gereken ek güvenlik prensipleri şunlardır:
- En Az Ayrıcalık İlkesi (Least Privilege): Uygulamanın kullandığı veritabanı kullanıcısına
DROP TABLE,ALTER TABLEveyaGRANTgibi yönetimsel yetkiler verilmemelidir. Yalnızca ihtiyaç duyulan tablolardaSELECT,INSERT,UPDATE,DELETEyetkisi tanımlanmalıdır. - Veritabanı Kimlik Bilgilerini Güçlendirme: Veritabanı şifreleri tahmin edilemeyecek karmaşıklıkta olmalıdır. Güvenli parola yapılandırması için parola üretici aracı üzerinden yüksek entropili anahtarlar türetilmelidir.
- Hata Mesajlarını Gizleme: Üretim ortamında veritabanı hata çıktıları (stack trace, tablo veya kolon adları) istemciye doğrudan döndürülmemeli, merkezi log sistemine kaydedilmelidir.
- Girdi Doğrulama ve Tiplendirme: Gelen veriler işleme alınmadan önce şema doğrulayıcılarla (Zod, Joi) doğrulanmalı, sayı bekleniyorsa integer türüne dönüştürülmelidir.
Sıkça Sorulan Sorular
Stored procedure kullanımı SQL injection açığını tamamen engeller mi?
Hayır. Stored procedure içinde dinamik SQL çalıştırılıyorsa (örneğin EXECUTE IMMEDIATE veya sp_executesql içine string birleştirilerek komut aktarılıyorsa) açık devam eder. Saklı yordamların güvenli olması için içerideki sorguların da parametreli biçimde yazılması zorunludur.
Sadece tek tırnakları kaçırmak veya temizlemek yeterli midir?
Yeterli değildir. Sayısal kolon filtrelerinde tırnak kullanılmadığı için (WHERE id = 5) tırnaksız enjeksiyon (5 UNION SELECT ...) mümkündür. Ayrıca karakter seti uyumsuzlukları (örneğin GBK veya özel UTF-8 dizilimleri) tırnak temizleme fonksiyonlarının atlatılmasına yol açabilir. Çözüm tırnak temizlemek değil, parametreli sorgu kullanmaktır.
ORM kullanan projelerde SQL injection riski tamamen biter mi?
Tamamen bitmez. ORM'in yerleşik filtreleme metodları güvenlidir; fakat geliştiriciler ham sorgu çalıştırma fonksiyonlarına (raw query) geçiş yaptığında string birleştirme kullanırsa aynı açık yeniden oluşur. Ham sorgularda parametre bağlama zorunluluğu devam eder.
Prepared statement sorgu performansını olumsuz etkiler mi?
Genellikle performansı artırır. Veritabanı motoru sorgu planını bir kez derler ve önbelleğe alır. Aynı sorgu farklı parametrelerle tekrar çalıştığında ayrıştırma ve optimizasyon adımları atlandığı için işlemci yükü azalır.