ORM'ler SQL Injection'a Karşı Ne Kadar Korur?
Modern ORM'ler parametreli sorguları varsayılan olarak üretir. Bu, klasik string birleştirme kaynaklı SQL injection riskini büyük ölçüde ortadan kaldırır. Ancak "ORM kullanıyorum, güvendeyim" varsayımı yanlıştır. ORM'ler sizi yalnızca kendi soyutlama katmanlarının içinde korur; bu katmanı terk ettiğiniz her noktada koruma da sona erer.
Bu rehber, ORM kullanan orta seviye geliştiricilerin injection açığı üretebileceği tipik noktaları ve her biri için uygulanabilir savunma yöntemlerini ele alır. Örnekler ORM'den bağımsız kalıpları göstermek için sözde kod ve yaygın API isimleri kullanır; kullandığınız ORM'nin tam API'si için kendi dokümantasyonuna bakın.
ORM'nin Koruduğu ve Korumadığı Yerler
ORM'ler tipik olarak şu işlemlerde parametreli sorgu üretir:
- Model üzerinden kayıt çekme (
where,find,filter) - Kayıt oluşturma ve güncelleme
- İlişkili kayıt sorguları
Bu işlemlerde kullanıcı girdisi sorgu metnine değil, ayrı parametre olarak gönderilir. Veritabanı sürücüsü değeri veri olarak işler, SQL parçası olarak değil.
Korumanın zayıfladığı veya bittiği yerler:
- Ham SQL çalıştıran API'ler (
raw,execute,query) - Sıralama, sütun adı, tablo adı gibi tanımlayıcıların dinamik üretildiği durumlar
- Sorgu oluşturucularda string parçası kabul eden metotlar
LIKE,INve benzeri operatörlerde yanlış kullanım- ORM dışı ek katmanlar: raporlama araçları, arama motorları, önbellek anahtarları
1. Ham Sorgularda Parametre Kullanımı
En sık yapılan hata, ORM'nin ham sorgu API'sine string birleştirmeyle değer geçirmektir.
Yanlış:
db.raw("SELECT * FROM users WHERE email = '" + email + "'")
Doğru:
db.raw("SELECT * FROM users WHERE email = ?", [email])
Beklenen davranış: İkinci örnekte email değeri sorgu metnine gömülmez; sürücüye ayrı parametre olarak iletilir. Kullanıcı ' OR '1'='1 gibi bir değer girse bile bu, SQL'in bir parçası olarak yorumlanmaz.
Adlandırılmış parametre destekleyen ORM'lerde aynı kural geçerlidir:
db.raw("SELECT * FROM users WHERE email = :email", { email })
Kritik nokta: Parametre yer tutucusu ile değerin her zaman ayrı tutulması. Değeri sorgu metnine "kaçış karakteri ekleyerek" sokmaya çalışmak güvenilir bir yöntem değildir.
2. Tanımlayıcılar Parametreleştirilemez
Sütun adı, tablo adı, ORDER BY yönü gibi SQL tanımlayıcıları parametre olarak gönderilemez. Bu yüzden geliştiriciler bunları genellikle string birleştirmeyle ekler ve açık üretir.
Yanlış:
sortColumn = req.query.sort
db.raw(`SELECT * FROM users ORDER BY ${sortColumn}`)
Burada saldırgan sort parametresine SQL parçası yerleştirebilir.
Doğru yaklaşım, izin verilen değerleri açıkça listelemektir:
const allowedSort = {
name: 'name',
created: 'created_at',
email: 'email'
}
const column = allowedSort[req.query.sort] ?? 'created_at'
db.raw(`SELECT * FROM users ORDER BY ${column}`)
Beklenen davranış: Kullanıcı girdisi hiçbir zaman doğrudan sorguya girmez; yalnızca önceden tanımlı bir anahtarın karşılığı olan sabit değer kullanılır. Girdi listede yoksa varsayılana düşer.
Aynı kural LIMIT, OFFSET gibi sayısal değerler için de geçerlidir: değeri sayıya çevirip doğrulayın, string olarak birleştirmeyin.
const limit = Math.min(Number(req.query.limit) || 20, 100)
3. LIKE Sorgularında Tuzak
LIKE kullanırken parametre kullanmak injection'ı engeller, ancak joker karakterler (%, _) hâlâ kullanıcı girdisinin parçası olabilir. Bu bir injection açığı değildir; ancak beklenmeyen sonuçlara ve bilgi sızıntısına yol açabilir.
db.raw("SELECT * FROM products WHERE name LIKE ?", ['%' + term + '%'])
Burada injection riski yoktur. Yine de % ve _ karakterlerini kaçırmak istiyorsanız, kaçış karakterini açıkça belirtmeniz gerekir:
db.raw(
"SELECT * FROM products WHERE name LIKE ? ESCAPE '\\\\'",
['%' + term.replace(/[%_\\\\]/g, '\\\\$&') + '%']
)
Beklenen davranış: Kullanıcının girdiği % karakteri joker olarak değil, aranan gerçek karakter olarak işlenir.
4. IN Listeleri
IN için değer listesini string birleştirmeyle kurmak yaygın bir hatadır.
Yanlış:
ids = req.query.ids.join(',')
db.raw(`SELECT * FROM orders WHERE id IN (${ids})`)
Doğru:
ids = req.query.ids.map(Number).filter(Number.isInteger)
placeholders = ids.map(() => '?').join(',')
db.raw(`SELECT * FROM orders WHERE id IN (${placeholders})`, ids)
Beklenen davranış: Yer tutucu sayısı, doğrulanmış değer sayısıyla eşleşir; değerler parametre olarak iletilir. Boş liste durumunu ayrıca ele almanız gerekir, çünkü IN () geçerli SQL değildir.
5. ORM Dışı Katmanlar
ORM'nin kendisi güvenli olsa bile uygulamanızın çevresindeki araçlar açık üretebilir:
- Raporlama ve BI araçları: Kullanıcı tanımlı filtreleri doğrudan SQL'e çeviren araçlar.
- Arama motorları: Sorgu dizesini kendi DSL'ine aktarırken doğrulama atlanması.
- Önbellek anahtarları: Kullanıcı girdisinin anahtara girmesi, farklı kullanıcıların aynı anahtarı paylaşmasına yol açabilir. Bu injection değil, veri sızıntısıdır.
- Dinamik filtre API'leri:
?filter[column]=valuegibi yapılarda sütun adının doğrulanmaması.
Bu katmanların her biri için girdiyi ORM'ye ulaşmadan önce doğrulayın.
Savunma Kontrol Listesi
- Ham sorgularda her zaman parametre kullanın; string birleştirme yapmayın.
- Tanımlayıcıları (sütun, tablo, yön) sabit listelerden seçin, kullanıcı girdisinden türetmeyin.
- Sayısal değerleri tip dönüşümü ve sınır kontrolünden geçirin.
LIKEveINiçin yer tutucu sayısını doğru üretin.- ORM dışı katmanlarda da aynı doğrulama kurallarını uygulayın.
- Üretimde veritabanı kullanıcısına yalnızca gereken yetkileri verin; injection başarılı olsa bile etkisi sınırlı kalsın.
- Hataları kullanıcıya ham SQL olarak döndürmeyin; şema bilgisi sızabilir.
Sonuç
ORM'ler injection riskini azaltır ama ortadan kaldırmaz. Güvenlik, ORM'nin sağladığı varsayılan davranışa değil, soyutlama katmanını terk ettiğiniz noktalardaki disipline bağlıdır. Ham sorgularda parametre kullanımı, tanımlayıcıların beyaz listeye bağlanması ve çevre katmanların aynı kurallara tabi tutulması, ORM tercihinden bağımsız olarak geçerliliğini koruyan üç temel kuraldır.