Loglanan sorguya bakıyorsunuz ve gönderilen şeyin kodda yazdığınızdan farklı olduğunu görüyorsunuz. Koşulun sonuna bir OR 1=1 yerleşmiş ya da sütununu sizin belirlemediğiniz bir ORDER BY. Kod Eloquent veya Hibernate veya Prisma kullanıyor, yani güvenli olmalı. Değil. ORM'de SQL enjeksiyonu bir çelişki değil; ORM'nin yalnızca kendisi sorgu string'ini oluşturduğunda parametreleme yapması gerçeğinin doğrudan sonucudur. Sizin string oluşturup ona teslim ettiğiniz her yerde koruma devre dışı kalır.
Bunun gerçekleştiği üç belirli nokta vardır. Üçü de günlük olarak yazılan koddadır ve hepsi kod incelemesinde sağlıklı görünür.
whereRaw ve kardeşleri
Her ORM'nin bir kaçış kapısı vardır. Laravel'de adı whereRaw'dır, TypeORM'de query() olur, SQLAlchemy'de text() olur. Bunlar query builder'ın üstesinden gelemediği ve ham SQL yazmanız gereken durumlar içindir. Sorun şu ki bu kaçış kapısının hiçbir koruması yoktur.
// bu enjeksiyona açıktır
$users = DB::table('users')
->whereRaw("email = '$email'")
->get();
// bu değildir
$users = DB::table('users')
->whereRaw('email = ?', [$email])
->get();
Fark bir soru işaretindedir. İlk sürüm string'i driver'a ulaşmadan önce oluşturur, dolayısıyla $email içindeki tırnak sorgu yapısını bozar. İkinci sürüm değeri parametre olarak bind eder ve driver onu kod değil veri olarak görür.
Burada hata yapıyorlar: birçok kişi girdiyi htmlspecialchars veya manuel bir escape fonksiyonuyla temizlemenin yeterli olduğunu düşünür. Bu hatanın işaretini testte değil, logda görürsünüz. Sorgu geliştirme ortamında doğru çalışır çünkü test girdinizde tırnak yoktur. Production'da ' içeren ilk istek ya syntax hatası verir ya da daha kötüsü, sessizce yanlış sonuç döndürür. Manuel escape de neredeyse her zaman eksiktir; her veritabanının kendi alıntılama kuralları vardır ve driver o kuralları sizden daha kesin bilir.
Kullanıcı girdisinden sütun adı ve sıralama yönü
Bu en ciddi durumdur, çünkü parametre bind hiç işe yaramaz. Placeholder yalnızca değer içindir. Sütun adı, tablo adı ve ASC/DESC kelimesi sorgu yapısının bir parçasıdır ve veritabanı bunların parametre olmasına izin vermez.
// ?sort=created_at&dir=desc
$sort = $request->query('sort');
$dir = $request->query('dir');
User::orderBy($sort, $dir)->paginate();
Eğer $sort doğrudan orderBy'a giderse, saldırgan istediği sütunu sıralayabilir ve sonuçların sırasından password_hash veya api_token gibi sütunların değerini çıkarabilir. Bu teknik kör değildir; birkaç yüz sıralı istekle değer bit bit sızar. ORM'niz orderBy'ı dahili doğrulamayla çalıştırmıyorsa, sütun adı yerine tam bir subquery bile yerleştirilebilir.
Doğru yol allow-list'tir. Tehlikeli karakterleri filtrelemek değil, sütun deseni üzerinde regex de değil.
$allowed = ['created_at', 'name', 'price'];
$sort = in_array($request->query('sort'), $allowed, true)
? $request->query('sort')
: 'created_at';
$dir = $request->query('dir') === 'asc' ? 'asc' : 'desc';
Sorgu yapısına giden her girdi "temizlenmek" yerine kapalı bir kümeden seçilmelidir. Bu ikisi arasındaki fark, allow-list'in doğası gereği güvenli, filtrelemenin ise doğası gereği kırılgan olmasıdır.
IN ve LIKE ve dinamik sıralamalar
Yeterince ciddiye alınmayan iki yer daha. Birincisi, girdiden oluşturulan bir diziyle whereIn: diziyi implode ile string'e çevirip whereRaw içine koyarsanız, aynı ilk soruna geri dönersiniz. whereIn('id', $ids) kendisi parametreler; whereRaw("id IN ($ids)") parametrelemez.
İkincisi LIKE. Değeri bind etseniz bile, kullanıcı girdisindeki % ve _ karakterleri wildcard anlamı taşır. Bu SQL enjeksiyonu değildir, ama hafif bir DoS saldırısıdır: desenin başındaki bir %, indeksi kullanılamaz hale getirir ve birkaç milyon satırlı bir tabloda sorguyu birkaç milisaniyeden birkaç saniyeye çıkarır. Sitenin araması yavaşladıysa ve veritabanı CPU'su yükseldiyse, her şeyden önce LIKE desenlerini kontrol edin.
ORM'yi atlamayın, ama ona da güvenmeyin
Tutumum net: query builder cevap verdiği sürece onu kullanın ve ham SQL'e gitmeyin. Mecbur kalırsanız, yalnızca placeholder ile. Placeholder işe yaramıyorsa (sütun adı gibi), allow-list. Bu sırayı değiştirmeyin.
Ama bu kararın bir maliyeti var ve bunu bilmelisiniz. Allow-list, sıralanabilir her sütun eklemesinin bir kod değişikliği gerektirdiği anlamına gelir. Bunu "bakımı imkansız" gören ekipler filtrelemeye yönelir ve altı ay sonra bir veri sızıntısı raporuyla karşılaşır. Allow-list'in bakım maliyetini ödeyin; kullanıcı tablosunu yeniden yazmaktan daha ucuzdur.
Bir operasyonel nokta da var: ORM hataları tam sorguyla loglar. Production'da APP_DEBUG açıksa, enjeksiyona açık olan sorgu bind değerleriyle birlikte HTTP yanıtında döner ve saldırgana yol haritasını verir. Bunu WordPress güvenliği; wp-config'ten kendileri açık olan eklentilere kadar da görmüştük: sızıntıların çoğu karmaşık bir güvenlik açığından değil, unutulmuş bir debug ayarından başlar.
Kodunuzun gerçekten savunmasız olup olmadığını nasıl anlarsınız
Üç iş, bu sırayla.
- Tüm kod tabanında
whereRaw,orderByRaw,selectRaw,DB::raw,query()vetext()arayın. Her çıktıyı elle okuyun. Bu grep on dakika sürer ve genellikle iki ila beş sonuç bulur. orderBy,groupBy,selectveya tablo adına giden her parametreyi, request'ten gelip gelmediğini görmek için izleyin.- Staging'de sqlmap gibi bir tarama aracıyla aynı endpoint'lerde test edin. Sıralama parametresini
--level=5 --risk=3ile verirseniz, genellikle birkaç dakika içinde sonuç alırsınız.
Denetim logunuz varsa, olağandışı sort= içeren istekleri veya uzun sorguları geriye dönük izleyebilirsiniz. Yoksa, Denetim logu nedir ve onu nasıl bozulmadan koruruz? doğru başlangıç noktasıdır, çünkü log olmadan saldırının başlayıp başlamadığını anlayamazsınız.
Ve birçok kişinin unuttuğu bir savunma katmanı: uygulamanın veritabanı kullanıcısında DROP veya FILE yetkisi olmamalıdır. Enjeksiyon başarılı olsa bile kullanıcının yalnızca birkaç tablo üzerinde SELECT ve INSERT yetkisi varsa, hasar alanı "tüm sunucu"dan "bir tablo"ya küçülür. Bu beş dakikalık ayar, en kötü senaryoyu katlanılabilir kılar.
Bu incelemeyi başka bir ekibe bırakmayı tercih ederseniz, ServerNet sunucu güvenlik hizmetleri koddaki ve sunucu yapılandırmasındaki tam da bu desenlerin incelenmesini içerir. Hizmetlerin anlık durumunu görmek için de Hizmet durumu sayfasını kontrol edin.
Sıkça sorulan sorular
ORM kullanmak tek başına SQL enjeksiyonunu engeller mi?
Hayır. ORM yalnızca sorgu string'ini kendisi oluşturduğunda ve değeri parametre olarak bind ettiğinde korur. Siz whereRaw veya DB::raw ile string oluşturup kullanıcı girdisini içine koyduğunuz anda, aynı ham SQL güvenlik açığı geri döner.
Basit kural: kodunuzda sorgu olması için . veya + ile birleştirilen her string, şüpheli noktadır.
Sütun adını neden placeholder ile parametreleyemiyorum?
Çünkü placeholder değerin yeridir, tanımlayıcının yeri değil. Veritabanı çalıştırmadan önce sorgu yapısını ayrıştırır ve sütun adı o aşamada belirli olmalıdır; parametre bind o aşamadan sonra doldurulur.
Yani sütun adı, tablo ve sıralama yönü için allow-list'ten başka çareniz yoktur. İzin verilen değerlerden sabit bir dizi oluşturun ve diğer her şeyi varsayılan değere döndürün.
Loglarda SQL enjeksiyon saldırısının belirtisi nedir?
Fazladan tek tırnak içeren sorgular, OR 1=1, UNION SELECT, koşulun sonunda -- veya /* yorumları ve iç içe URL kodlamalı parametreler. Sunucu düzeyinde de bir endpoint üzerinde farklı sıralama parametreleriyle art arda gelen istekler, klasik kör çıkarma desenidir.
Ayrı ve bozulmamış bir denetim logunuz varsa, hangi sorguların gerçekten çalıştırıldığını anlayabilir ve sızıntının kapsamını sınırlayabilirsiniz.
WAF bu tür enjeksiyonu engeller mi?
Bir kısmını engeller, ama hepsini değil. WAF girdi parametrelerindeki bilinen desenler üzerinde çalışır ve basit bir kelime biçiminde gelen sıralama sütun adı üzerinden yapılan enjeksiyonu genellikle görmez. DDoS koruması ve WAF, yük azaltma katmanlarıdır, koddaki allow-list'in yerine geçmez.
Doğru sıra şudur: önce kodu düzeltin, sonra ağ katmanını ekleyin. Tersi yalnızca zaman kazandırır.
Yorumlar 0
Henüz yorum yok — ilk siz olun!