SQL Injection, bir saldırganın sorgu parametrelerine kötü amaçlı SQL kodu enjekte ederek verilere yetkisiz erişim veya bunları değiştirme yeteneği elde ettiği bir veritabanı saldırı türüdür. OWASP (2025)'e göre SQL Injection, veritabanının tamamen tehlikeye girmesine yol açabilen kritik güvenlik açıklarından biri olmaya devam etmektedir. SQL kodu enjeksiyonu, saldırganın kayıtları okumasına, değiştirmesine ve silmesine ve bazı durumlarda sunucunun işletim sistemine erişmesine olanak tanır.
Önemli Noktalar
SQL Injection, bir uygulamanın kullanıcı verileriyle dize birleştirmesi yaparak SQL sorguları oluşturduğunda ortaya çıkan bir güvenlik açığıdır. Bir saldırgan, SQL komutunun yapısını değiştiren özel olarak hazırlanmış bir dizeyi sorgu parametresine iletir. Enjekte edilen dize, veri olmak yerine SQL kodunun bir parçası haline gelir ve saldırganın veritabanına karşı keyfi sorgular yürütmesine olanak tanır. Verizon Veri İhlali Raporu (2025)'e göre SQL Injection, araştırılan tüm veri ihlallerinin %8'inde mevcuttur.
Bilinen bir güvenlik açığı olmasına rağmen (ilk olarak 1990'ların sonunda bahsedilmiştir), SQL Injection modern uygulamalarda hala bulunmaktadır. Bunun nedeni insan hatasıdır: geliştiriciler dize birleştirmeli kod yazar, eski kod yeniden düzenlenmez ve ORM çerçeveleri yanlış kullanılır (örneğin, dize enterpolasyonlu ham sorgular). Veracode (2026)'ya göre taranan tüm uygulamaların yaklaşık %14'ü en az bir SQLi güvenlik açığı içermektedir.
Başarılı bir SQL Injection, saldırgana geniş bir yetenek yelpazesi sağlar: parola karmaları ve kullanıcıların kişisel verileri dahil olmak üzere herhangi bir veritabanı tablosunu okumak; kayıtları değiştirmek ve silmek; yönetim işlemlerini yürütmek (DROP TABLE, TRUNCATE); ve bazı yapılandırmalarda xp_cmdshell (MSSQL) veya INTO OUTFILE (MySQL) aracılığıyla uzaktan komut yürütme. Sonuçlar, kullanıcı verilerinin sızmasından sistem kontrolünün tamamen kaybedilmesine kadar uzanır.
SQL enjeksiyonları, veritabanından veri çıkarma yöntemine göre sınıflandırılır. Yöntemin seçimi, uygulamanın sorgu sonuçlarını ve hata mesajlarını nasıl işlediğine bağlıdır. OWASP sınıflandırması üç ana türü tanımlar: In-band (aynı kanal aracılığıyla veri çıkarma), Inferential/Blind (mantıksal çıkarımlar) ve Out-of-band (başka bir kanal aracılığıyla veri iletimi).
| Tür | Veri Çıkarma Yöntemi | Karmaşıklık | Sıklık |
|---|---|---|---|
| In-band (klasik) | Doğrudan sorgu sonucu aracılığıyla | Düşük | Yüksek |
| Blind SQLi | Sunucu yanıtlarından mantıksal çıkarımlar | Yüksek | Orta |
| Out-of-band | Harici kanal (DNS, HTTP) aracılığıyla | Orta | Düşük |
En yaygın tür. Bir saldırgan, sorgu parametresine SQL kodu enjekte eder ve enjeksiyonun sonucu doğrudan sunucu yanıtında görünür. İki alt tür: Error-based (DB hata mesajları aracılığıyla) ve UNION-based (UNION SELECT operatörü aracılığıyla). Error-based, tablo adını veya sorgu yapısını ortaya çıkarabilecek MySQL sözdizimi hatası gibi hata mesajlarındaki bilgileri kullanır. UNION-based, meşru sorgu sonuçlarını veritabanındaki diğer tablolardaki verilerle birleştirmeye olanak tanır.
Uygulamanın sorgu sonuçlarını veya hata mesajlarını göstermediği durumlarda kullanılır. Bir saldırgan, mantıksal koşullar içeren sorgular göndererek ve sunucu yanıtlarındaki farklılıkları (örneğin, yanıt süresi veya sayfa içeriği) analiz ederek evet/hayır soruları sorar. Time-based Blind SQLi, koşulları onaylamak için gecikme işlevlerini (SLEEP, WAITFOR DELAY) kullanır — sayfanın yüklenmesi daha uzun sürerse koşul doğrudur. Bu yöntem çok yavaştır — tek bir kaydı çıkarmak saatler alabilir.
# Blind SQL Injection Örneği (zaman tabanlı)
# SQLi savunmasızsa, koşul karşılandığında SLEEP(2) yürütülür
import requests
import time
payload = "' OR IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',SLEEP(2),0) -- "
url = "http://example.com/user?id=1" + payload
start = time.time()
response = requests.get(url)
elapsed = time.time() - start
# Yanıt >2 saniye sonra gelirse — parolanın ilk harfi 'a'
print(f"Time: {elapsed:.2f}s - First letter is 'a'" if elapsed > 2 else "First letter is not 'a'")
Veriler HTTP yanıtı aracılığıyla değil, alternatif kanallar (DNS sorguları, harici bir sunucuya HTTP istekleri, SMTP) aracılığıyla iletilir. Uygulamanın sorgu sonuçlarını döndürmediği veya hata göstermediği durumlarda kullanılır. MySQL, DNS sorgusu başlatabilen LOAD_FILE() işlevini destekler ve MSSQL, uzak bir SMB sunucusuna veri göndermek için xp_dirtree'ye sahiptir. Out-of-band SQL Injection etkilidir ancak ek saldırgan sunucu kurulumu ve belirli DB işlevleri gerektirir.
SQL Injection mekanizması, SQL'in dize değişmezleri için tırnak işaretleri kullanmasına dayanır. Bir uygulama, kullanıcı girişini kaçış karakterleri olmadan doğrudan bir SQL sorgusuna eklerse, saldırgan dizeyi “kapatabilir” ve isteğe bağlı SQL kodu ekleyebilir. Örneğin, SELECT * FROM users WHERE name = '$input' sorgusuna ' OR '1'='1 eklenmesi, onu SELECT * FROM users WHERE name = '' OR '1'='1' haline getirir ve tüm kullanıcıları döndürür.
SELECT * FROM users WHERE username = '$user' AND password = '$pass' sorgusunu kullanan bir giriş formu düşünün. Bir saldırgan, kullanıcı adı alanına admin' -- girer ve parolayı boş bırakırsa, sonuçtaki sorgu SELECT * FROM users WHERE username = 'admin' -- ' AND password = '' olur. -- karakterleri sorgunun geri kalanını yorum haline getirerek parola kontrolünü devre dışı bırakır. Sunucu, admin kullanıcı kaydını döndürür ve saldırgan parolayı bilmeden giriş yapar.
# SQL Injection Örneği — Kimlik Doğrulama Atlaması
# SAVUNMASIZ KOD: doğrudan dize birleştirmesi
def login_vulnerable(username, password):
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
cursor.execute(query)
return cursor.fetchone() is not None
# username = "admin' --" parola kontrolünü geçersiz kılar
# GÜVENLİ SÜRÜM: parametreleştirilmiş sorgu
def login_secure(username, password):
query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))
return cursor.fetchone() is not None
UNION SELECT operatörü, iki SELECT sorgusunun sonuçlarını birleştirmeye olanak tanır. Bir saldırgan savunmasız bir parametre bulursa, başka bir tablodan sorgu ile UNION SELECT ekler. Örnek: ' UNION SELECT username, password FROM admins --. Başarı koşulu, her iki sorgudaki sütun sayısının eşleşmesidir. Sütun sayısı ORDER BY aracılığıyla belirlenir (' ORDER BY 1--, ardından 2, 3... hata oluşana kadar). Sütun sayısını bilen saldırgan, aynı sayıda alanla UNION SELECT ekler.
Mobil uygulamalar, hem sunucu tarafında (API) hem de istemci tarafında — yerel veritabanlarında (SQLite, Realm) — SQL Injection ile karşı karşıyadır. Mobil API'lerdeki sunucu tarafı SQLi, web uygulamalarına benzerken, yerel veritabanları ek bir vektör oluşturur. Bir uygulama SQLite'da veri depoluyor ve dize birleştirme ile sorgular yürütüyorsa, API aracılığıyla yerel veritabanına giren kötü amaçlı veriler, sonraki işleme sırasında SQLi'yi tetikleyebilir.
Cihazdaki yerel SQLite veritabanı da, uygulama dizeleri birleştirerek sorgular oluşturuyorsa SQL Injection'a karşı savunmasızdır. Android'deki Content Providers ve iOS'teki Core Data varsayılan olarak parametreleştirme kullanır, ancak ham sorgular geliştiricinin dikkatini gerektirir. SQLite, noktalı virgülle ayrılmış birden çok sorguyu desteklemez, bu da saldırganın seçeneklerini sınırlar ancak WHERE koşulları aracılığıyla veri okumaya karşı koruma sağlamaz. Android'de her zaman selectionArgs ve iOS'te parametrelerle NSPredicate kullanın.
Bir mobil uygulamanın iletişim kurduğu API, herhangi bir web sunucusu gibi savunmasızdır. Mobil geliştiriciler genellikle SQLi'nin yalnızca bir backend sorunu olduğunu varsayar, ancak güvenlik açığı, istemciden parametreleri kabul eden API uç noktasında oluşur. Sorumlulukların ayrılması koruma sağlamaz: backend geliştiricisi sorguyu parametreleştirmeyi unuttuysa, kullanıcının mobil uygulaması bir saldırı vektörü haline gelir. Backend'in ORM veya prepared statements kullanmasını talep edin.
// Android'de Yerel SQLite'da SQL Injection Örneği
// SAVUNMASIZ KOD: doğrudan birleştirme
fun getUserVulnerable(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = $userId", null
)
}
// GÜVENLİ SÜRÜM: selectionArgs aracılığıyla parametreleştirme
fun getUserSecure(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = ?",
arrayOf(userId)
)
}
SQL Injection'a karşı koruma basit bir ilkeye dayanır: SQL sorgularında kullanıcı girişine asla güvenmeyin. Güvenilir tek yöntem, SQL kodu ve verilerin ayrı ayrı iletildiği sorgu parametreleştirmesidir (prepared statements). Diğer tüm yöntemler — kaçış, doğrulama, WAF — ek güvenlik katmanlarıdır ancak parametreleştirmenin yerini tutmaz. OWASP (2025)'e göre parametreleştirme, SQL Injection saldırılarının %100'ünü önler.
Prepared statements kullanılırken, SQL sorgusu önce DB sunucusu tarafından veri olmadan derlenir ve ardından parametre değerleri ayrı ayrı iletilir. Veritabanı, parametreleri yürütülebilir kod olarak değil, veri olarak ele alır. Bir saldırgan ' OR '1'='1 iletse bile, veritabanı bunu SQL kodu olarak değil, bir dize değeri olarak yorumlar. Prepared statements, PHP'de PDO, Java'da PreparedStatement, Python'da cursor.execute gibi tüm modern diller ve çerçeveler tarafından desteklenir.
Modern ORM'ler (Hibernate, Entity Framework, SQLAlchemy, Room), geliştirici ham sorgulara geçmediği sürece sorguları yürütürken otomatik olarak parametreleştirme kullanır. Ancak, ORM'ler tamamen koruma sağlamaz: JPA'daki @Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true) gibi yapılar, parametrelerin birleştirme yoluyla değil, adlandırılmış parametreler aracılığıyla iletilmesini gerektirir. Sorgu oluşturucular (Knex, jOOQ) da ham yöntemler kullanılmadığında varsayılan olarak sorguları parametreleştirir.
Özel karakterlerin kaçışı (mysql_real_escape_string), tüm SQL Injection türlerine karşı koruma sağlamayan eski bir yöntemdir. Sorun: kaçış, kodlamaya bağlıdır ve çok baytlı kodlamalar (örneğin, Asya sistemlerinde GBK) kullanılırken atlatılabilir. Kaçışı yalnızca parametreleştirmenin mümkün olmadığı eski kodlarda ve her zaman sıkı giriş türü doğrulamasıyla birlikte kullanın.
| Yöntem | Etkinlik | Öneri |
|---|---|---|
| Prepared Statements | 100% | Tüm sorgular için zorunlu |
| ORM (doğru kullanım) | 99% | Önerilir |
| Dize Kaçışı | 70% (kodlamaya bağlı) | Yalnızca eski |
| Giriş Doğrulaması (beyaz liste) | 50% (yalnızca sayılar) | Ek |
| WAF (Web Application Firewall) | 60% | Ek |
Uygulama hesabı, minimum gerekli ayrıcalıklara sahip olmalıdır: SELECT, INSERT, UPDATE, DELETE — yalnızca uygulamanın gerçekten ihtiyaç duyduğu tablolarda. Uygulama hesabı için DROP, TRUNCATE, CREATE kullanımını yasaklayın. Bu, başarılı bir SQL Injection durumunda bile hasarı sınırlar: saldırgan tabloları silemez veya yönetim işlemlerini yürütemez.
Düzenli SQL Injection testi, güvenli geliştirme hattının bir parçası olmalıdır. Statik analiz, dinamik tarama ve manuel sızma testinin bir kombinasyonu en iyi sonuçları verir. Synopsys Siber Güvenlik Raporu (2025)'e göre, otomatik tarayıcılar SQLi güvenlik açıklarının %70'ine kadarını bulur, ancak karmaşık Kör saldırılar manuel test gerektirir.
Mobil uygulamalar için yerel SQLite analizi de önemlidir: tüm rawQuery çağrılarını, ContentProvider sorgularını ve rawQuery ile Room sorgularını kontrol edin. Araçlar: Android Studio Lint (SQLite'da SQLi'yi tespit eder), APK/IPA'nın otomatik statik ve dinamik analizi için MobSF (Mobile Security Framework). Ayrıca mobil uygulama trafiğinin proxy müdahalesiyle sqlmap aracılığıyla API uç noktalarını test etmeniz önerilir.
Sıkça Sorulan Sorular
SQL Injection, SQL sorguları aracılığıyla ilişkisel veritabanlarına saldırır. NoSQL Injection, sorgu operatörleri ($gte, $ne, $where) aracılığıyla ilişkisel olmayan veritabanlarını (MongoDB, Couchbase) etkiler. MongoDB'de, uygulama bir JSON dizesinden BSON belgesi oluşturuyorsa enjeksiyon mümkündür. Koruma mekanizmaları benzerdir: parametreleştirme ve tür doğrulaması.
SQL sorgularının kullanıcı verileriyle dize birleştirmesi yoluyla oluşturulduğu tüm yerleri bulun. "SELECT ... WHERE id = " + userId veya f"UPDATE ... SET name = '{name}'" gibi kalıplar arayın. Bu tür her satır potansiyel bir SQL enjeksiyonudur. Tümünü parametreleştirilmiş sorgular veya prepared statements ile değiştirin.
ORM çerçeveleri yalnızca Sorgu Oluşturucu yöntemlerini ve adlandırılmış parametreleri kullandığınızda otomatik olarak korur. Ham sorgular (JPA'da nativeQuery, Room'da rawQuery) kullanıyorsanız, koruma çalışmaz — parametreleri dize birleştirmesi yoluyla değil, hazırlanmış ifadeler aracılığıyla iletmeniz gerekir.
Second-Order SQL Injection, kötü amaçlı verilerin veritabanında güvenli bir şekilde depolandığı ancak daha sonra kaçış olmadan başka bir sorguda kullanıldığı bir saldırıdır. Örneğin, bir saldırgan ' OR '1'='1 gibi bir kullanıcı adıyla kaydolur. Veriler bir dize olarak kaydedilir — kayıt sırasında saldırı olmaz. Ancak başka bir sorgu, parametreleştirme olmadan SQL'de bu kullanıcı adını kullanırsa, enjeksiyon tetiklenir.
NoSQL Injection, geliştirici farkındalığının daha düşük olması nedeniyle potansiyel olarak daha tehlikeli olabilir. Geliştiriciler SQL Injection'ı bilir ve çoğu ORM kullanır, ancak çok azı NoSQL Injection'ı bilir. MongoDB'de yanlış biçimlendirilmiş bir sorgu, bir koleksiyondaki tüm belgeleri döndürebilir. Koruma aynıdır — prepared statements (BSON parametreleştirmesi) ve sıkı giriş doğrulaması.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun