SQL Injection: Aslında Bir String Birleştirme Hatası
SQL injection nedir sorusunun en sade cevabı şu: kullanıcıdan gelen verinin, veritabanının ayrıştırdığı SQL metninin bir parçası hâline gelmesidir. O sınır bir kez bulanıklaştığında — yani kullanıcının yazdığı bir değer, veritabanının çalıştırdığı sözdizimine dönüştüğünde — kullanıcı, sizin yapabildiğiniz her şeyi yapabilir hâle gelir.
İşte hemen her dilde karşımıza çıkabilecek klasik anti-pattern, sözde kodla:
-- BUNU YAPMAYIN
query = "SELECT * FROM users WHERE name = '" + user_input + "'"
Eğer user_input değeri Ada ise normal bir sorgu çalışır. Ancak user_input değeri ' OR 1=1 -- olursa şu sorgu ortaya çıkar:
SELECT * FROM users WHERE name = '' OR 1=1 --'
-- ile sondaki tırnak yorum satırına dönüşür, OR 1=1 her satırı eşleştirir ve saldırgan kullanıcı tablonuzu çekip gider. Daha kötü senaryolarda ; ile ikinci bir sorgu zincirlenir; tablolar silinir, veri sızdırılır ya da yeni bir admin hesabı eklenir.
Açık SQLite'ta değil. Açık, o stringi inşa eden kodda.
SQL Injection Önlemenin Asıl Yolu: Parametreli Sorgular
SQLite parametreli sorgu, SQL metnini değerlerden ayırır. SQL tarafında yer tutucular bulunur — ? veya :name — ve değerleri ayrı bir şekilde geçirirsiniz. SQLite, SQL'i bir kez parse edip derler, ardından değerlerinizi derlenmiş plana bağlar (bind eder). Değerler hiçbir şekilde SQL'e dönüşemez.
Riskli görünen bir sorguyu güvenli biçimde çalıştıralım:
SQLite kabuğunda değeri doğrudan elle yazıyorsunuz; ama uygulama kodunda aynı şeyin karşılığı şöyle görünür (Python'un sqlite3 sürücüsüyle):
# Python — parametreli, güvenli
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
SQL'i ve değerler tuple'ını iki ayrı argüman olarak gönderin. Sürücü bunları SQLite'a ayrı ayrı iletir. user_input değeri ' OR 1=1 -- olsa bile, SQLite tam olarak ' OR 1=1 -- adında bir kullanıcı arar ve doğal olarak bulamaz.
Buradaki "Güvenli" Olmak Ne Anlama Geliyor?
Güvenliğin sırrı kalıp eşleştirme ya da escape işlemi değil; tamamen yapısal bir konu. SQLite, değerinizi daha görmeden önce ifadeyi kendi iç biçimine derliyor:
-- Derlenmiş ifadenin bir yuvası vardır, dize değil.
SELECT * FROM users WHERE name = ?
^
yer tutucu yuvası
Bir değeri bind ettiğinizde, o değer ilgili slota tipi belli bir veri olarak yerleşir — TEXT, INTEGER, BLOB, ne olursa. SQLite bunu bir daha SQL olarak parse etmez. Saldırganın enjekte edebileceği bir sözdizimi yoktur, çünkü parser işini çoktan bitirmiştir.
İşte sqlite parametreli sorgu yaklaşımının, karakter kaçışlamanın (escaping) asla yakalayamayacağı bir güvenilirliği bu yüzden vardır. Escaping, tehlikeli karakterleri bir string'in içinden temizlemeye çalışır. Bind etmek ise tehlikeli string'i en baştan oluşturmaz.
String Birleştirmeye Sakın Yeltenmeyin
Her dilin cazip bir kestirme yolu vardır — Python'da f-string'ler, JavaScript'te template literal'lar, Java'da String.format — ve hepsi SQL söz konusu olunca elinizde patlayan birer bombadır.
# YAPMAYIN — f-string değeri SQL metnine enterpole eder
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
# YAPMAYIN — aynı sorun, % biçimlendirmesi
cursor.execute("SELECT * FROM users WHERE name = '%s'" % user_input)
# YAPIN — yer tutucu + values argümanı
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
İlk iki yöntemde, kullanıcı girdisi sürücü daha sorguyu görmeden SQL metnine yapıştırılır. SQLite sorguyu elinize aldığında iş işten geçmiş olur. Üçüncü yöntem ise SQL'i ve değeri ayrı şeritlerde tutar.
Bu kural mekaniktir: Kendinizi +, f-string, format ya da template literal kullanarak içine değer yerleştirilmiş bir SQL stringi kurarken yakalarsanız — hemen durun ve yerine placeholder kullanın.
Birden Fazla Parametre ve İsimli Placeholder Kullanımı
Gerçek hayatta sorgular genellikle birden fazla değer içerir. SQLite hem konuma göre çalışan ? hem de isimli :name placeholder'larını destekler:
Uygulama kodunda bunlar şuna karşılık gelir:
# Konumsal
cursor.execute(
"SELECT * FROM orders WHERE customer = ? AND status = ?",
("Ada", "paid"),
)
# Adlandırılmış — birkaç parametre olduğunda daha anlaşılır
cursor.execute(
"SELECT * FROM orders WHERE total > :min_total AND status = :status",
{"min_total": 50, "status": "paid"},
)
İsimlendirilmiş parametreler büyüdükçe daha çok işe yarar. Üç dört değeri geçtiniz mi, ?, ?, ?, ? resmen bilmece oyununa dönüşüyor; :customer, :total, :status, :created_at ise kendi kendini belgeliyor.
Tablo ve Sütun Adları İçin Farklı Bir Yol Lazım
Bağlanan parametreler yalnızca değerler için çalışır — yani = işaretinin sağındaki ifadeler, IN (...) içindekiler, VALUES (...) içine yazılanlar. Tablo adları, sütun adları ya da ASC/DESC gibi SQL anahtar kelimeleri için bu yöntem işe yaramaz.
-- Bu ÇALIŞMAZ. Yer tutucu, bir sütun adının yerine geçemez.
SELECT * FROM users ORDER BY ? ASC
Eğer sorguya dinamik bir tanımlayıcı eklemeniz gerekiyorsa — mesela kullanıcının hangi sütuna göre sıralama yapacağını seçmesine izin veriyorsanız — SQL'i oluşturmadan önce mutlaka beyaz liste (allowlist) ile doğrula:
# İzin verilenler listesi yaklaşımı
ALLOWED_SORT_COLUMNS = {"name", "created_at", "role"}
if sort_column not in ALLOWED_SORT_COLUMNS:
raise ValueError(f"Geçersiz sıralama sütunu: {sort_column}")
query = f"SELECT * FROM users ORDER BY {sort_column} ASC"
cursor.execute(query)
Kullanıcının verdiği string, SQL'e yaklaşmadan önce sabit ve güvenli bir değer listesiyle karşılaştırılıyor. Buradaki f-string'in kabul edilebilir olmasının tek sebebi de bu: sort_column artık üç sabit isimden biri olmak zorunda, başka bir şey olamaz.
Gerçek bir SQL injection denemesi ve nasıl etkisiz kaldığı
Şimdi iki yaklaşımı kötü niyetli bir girdiyle yan yana görelim. Önce küçük bir users tablosu hazırlayalım:
Açık formdaki sorgu bütün kullanıcıları döndürüyor. Parametreli sürüm ise tam olarak ' OR 1=1 -- adında bir kullanıcı arıyor ve hiçbir şey bulamıyor. Aynı girdi, tamamen farklı sonuç — çünkü ikinci durumda değer hiçbir zaman SQL'e dönüşmedi.
Kısa Bir Kontrol Listesi
- Kodunuzun dışından gelen her değer için
?ya da:nameplaceholder kullanın — kullanıcı girdisi, request body, ortam değişkenleri, satır içine sizin yazmadığınız ne varsa. - Değerin gittiği yerde
+, f-string veyaformatile SQL kurmayın. Asla. - Dinamik tablo veya sütun isimlerinde, sorguya yapıştırmadan önce sabit bir izin listesine (allowlist) karşı doğrulama yapın.
- Sürücüye güvenin. Kendi kaçış (escape) fonksiyonunuzu yazmayın. Bind parametre mekanizması daha eski, savaşta sınanmış ve doğru olanı yapıyor.
- Ekibinizin sorgularını gözden geçirirken tek soru sorun: SQL metnine bir kullanıcı girdisi mi yapıştırılıyor? Cevap "evet"se düzeltin.
Bu alışkanlık parmaklarınıza yerleştiğinde, SQL injection artık üzerinde düşünmek zorunda olduğunuz bir hata sınıfı olmaktan çıkar.
Sırada: Uygulamalardan Bağlanmak
Güvenli sorgunun şeklini gördünüz — placeholder SQL'in içinde, değer ise yanından geçiyor. Bir sonraki sayfada SQLite'ı Python, Node.js ve birkaç dil daha ile gerçek uygulama kodundan nasıl bağlayacağımızı; bağlantı yönetimini ve parametreli sorguların tipik bir request akışında nereye oturduğunu adım adım göreceğiz.
Sıkça Sorulan Sorular
SQLite SQL injection'a karşı savunmasız mı?
Evet. Uygulama kodunuz sorguları string birleştirerek kuruyorsa, SQLite da diğer SQL veritabanları kadar açıktır. Çözüm SQLite tarafında bir ayar değil — değerleri uygulamadan veritabanına nasıl ilettiğinizle ilgili. ? veya :isim placeholder'ları ile parametreli sorgu kullanın, gerisini sürücü güvenli şekilde halleder.
Parametreli sorgular SQL injection'ı nasıl engelliyor?
? gibi placeholder kullandığınızda SQLite önce sorguyu parse edip derliyor, sonra değerlerinizi zaten derlenmiş ifadedeki yuvalara bağlıyor. Yani değerler hiçbir zaman SQL söz dizimine dönüşemez — sadece veri olarak işlenir. Saldırganın kıracağı bir string ortada yoktur.
Bunun yerine kullanıcı girdisindeki tırnakları escape etsem olmaz mı?
Olmaz. Manuel escape kırılgandır — eninde sonunda bir uç durumu (Unicode tırnaklar, encoding oyunları, yorum işaretleri) atlar ve açık bir kod yayınlarsınız. Sürücüler ? ve :isim parametrelerini tam olarak siz escape ile uğraşmayın diye sunuyor. 'Güvenli olduğunu bildiğiniz' değerlerde bile her zaman bunları kullanın.
Peki kullanıcıdan gelen tablo veya kolon isimleri için ne yapmalı?
Bound parametreler yalnızca değerler için çalışır, identifier'lar için değil. Tablo veya kolon ismi dinamik olmak zorundaysa, SQL'e koymadan önce bilinen isimlerden oluşan bir allowlist'e karşı doğrulayın. Kullanıcıdan gelen ham bir identifier'ı asla string formatlama ile sorguya sokmayın.