August 23, 2026
SQL Injection’ı Anlamak:
Bir Parametrenin SQL Sorgusuna Dönüşmesi
By Sena Şahin
6 min read
Bir Parametrenin SQL Sorgusuna Dönüşmesi
Web güvenliğiyle ilgilenmeye başladığımda SQL Injection, karşıma çıkan ilk ve en bilinen web zafiyetlerinden biriydi. İlk başta SQL Injection'ı yalnızca kullanıcı adı alanına birkaç karakter ekleyip giriş yapmaya çalışmak gibi düşünüyordum ama laboratuvarlarda farklı uygulamalar üzerinde çalıştıkça bunun aslında tek bir payload'dan çok daha fazlası olduğunu fark ettim.
SQL Injection'ın temelinde çok basit bir problem var:
Uygulamanın kullanıcıdan aldığı veriyi SQL kodundan ayıramaması.
Bu yazıda SQL Injection'ın nasıl ortaya çıktığını, bir zafiyetin nasıl tespit edildiğini, farklı SQLi türlerinin nasıl çalıştığını ve geliştirici tarafında nasıl engellenmesi gerektiğini ele alacağım.
SQL Injection Nedir?
Bir web uygulamasının kullanıcıdan aldığı veriyi bir SQL sorgusunun içerisine doğrudan eklediğini düşünelim.
Örneğin bir ürün sayfası şu şekilde çalışıyor olsun:
SELECT * FROM products WHERE id = 10;SELECT * FROM products WHERE id = 10;Buradaki 10 değeri URL'deki id parametresinden geliyor olabilir:
/products?id=10/products?id=10Eğer geliştirici bu değeri kontrol etmeden sorguya ekliyorsa kod kabaca şöyle bir yapıya dönüşebilir:
const id = req.query.id;
const query = `SELECT * FROM products WHERE id = ${id}`;const id = req.query.id;
const query = `SELECT * FROM products WHERE id = ${id}`;Normal durumda kullanıcı 10 gönderdiğinde:
SELECT * FROM products WHERE id = 10;SELECT * FROM products WHERE id = 10;oluşur.
Problem, uygulamanın gönderilen değerin gerçekten yalnızca bir ID olduğunu varsaymasıdır.
Kullanıcının gönderdiği veri SQL sorgusunun bir parçası haline gelebiliyorsa, kullanıcı artık sadece veri göndermemiş olur. Sorgunun mantığını da değiştirebilir.
SQL Injection'ın temel mantığı tam olarak burada ortaya çıkar.
Önce Uygulamanın Davranışını Anlamak
Bir SQL Injection testi yaparken doğrudan karmaşık payload'lara geçmek çoğu zaman gereksizdir.
Önce uygulamanın normal davranışını anlamak daha önemlidir.
Örneğin:
?id=10?id=10isteğine karşılık ürün 10 dönüyorsa, daha sonra farklı girdiler göndererek uygulamanın parametreyi nasıl işlediğini gözlemleyebiliriz.
Örneğin:
?id=10-1?id=10-1gönderildiğinde uygulama ürün 9'u getiriyorsa, burada önemli bir davranış gözlemlemiş oluruz.
Çünkü uygulama parametreyi yalnızca "10-1" şeklinde bir değer olarak ele almamış, bunu SQL tarafında değerlendirilen bir expression haline getirmiş olabilir.
Benzer şekilde:
?id=10+1?id=10+1gibi girdiler de uygulamanın parametreyi nasıl işlediği hakkında fikir verebilir.
Ancak burada önemli bir nokta var:
Tek bir davranış SQL Injection'ın kesin kanıtı değildir.
Uygulamanın kendi routing veya input parsing mekanizması da benzer sonuçlar üretebilir. Bu yüzden testleri karşılaştırmalı yapmak gerekir.
' Karakteri Neden Bu Kadar Önemli?
SQL Injection denildiğinde en sık karşılaşılan karakterlerden biri tek tırnaktır:
''Bunun sebebi SQL'deki string değerlerin çoğunlukla tek tırnak içerisinde tutulmasıdır.
Örneğin:
SELECT * FROM users WHERE username = 'sena';SELECT * FROM users WHERE username = 'sena';Burada sena bir string değeridir.
Eğer kullanıcı girdisi doğrudan sorguya ekleniyorsa:
sena'sena'gibi bir değer sorgunun yapısını değiştirebilir.
Örneğin uygulamanın oluşturduğu sorgu şu hale gelebilir:
SELECT * FROM users WHERE username = 'sena'';SELECT * FROM users WHERE username = 'sena'';Bu durumda veritabanı sorguyu parse etmeye çalışırken syntax error üretebilir.
Eskiden SQL Injection tespitinde bu tür hatalar oldukça kullanışlıydı. Günümüzde ise uygulamalar çoğu zaman veritabanı hatalarını kullanıcıya göstermiyor.
Dolayısıyla:
SQL syntax errorSQL syntax errorgörememek SQL Injection olmadığı anlamına gelmez.
Tam tersine, bizi blind SQL Injection gibi yöntemlere götüren noktalardan biri budur.
Boolean-Based SQL Injection
Boolean-Based Blind SQL Injection'da veritabanından doğrudan veri okuyamayız.
Bunun yerine veritabanına sorular sorarız.
Sorunun cevabı:
TRUETRUEveya
FALSEFALSEolur.
Ancak uygulama bize doğrudan TRUE veya FALSE yazmaz. Bunun yerine davranışı değişir.
Örneğin:
?id=1 AND 1=1?id=1 AND 1=1ile:
?id=1 AND 1=2?id=1 AND 1=2isteklerini karşılaştırabiliriz.
Eğer ilk istekte ürün görünürken ikinci istekte görünmüyorsa, uygulamanın sorgunun sonucuna göre farklı davrandığını anlamış oluruz.
Bu noktada saldırganın elinde aslında bir evet/hayır kanalı vardır.
Örneğin:
Veritabanındaki mevcut kullanıcının ilk karakteri
Amı?
Hayır.
Bmi?
Hayır.
Cmi?
Evet.
Bu yaklaşım tek tek karakterleri tahmin ederek veri çıkarmaya kadar ilerleyebilir.
Neden Binary Search Kullanılabilir?
Blind SQL Injection'ın en ilginç taraflarından biri burada başlıyor.
Bir karakteri bulmak için bütün karakterleri tek tek denemek zorunda değiliz.
Örneğin ASCII değerlerinin belirli bir aralıkta olduğunu biliyorsak, karakterin:
> 77> 77olup olmadığını sorabiliriz.
Cevap TRUE ise aralığın üst yarısına, FALSE ise alt yarısına geçebiliriz.
Örneğin:
32 - 12632 - 126aralığını ele alalım.
İlk soru:
ASCII değeri 79'dan büyük mü?ASCII değeri 79'dan büyük mü?Sonuca göre aralığı ikiye böleriz.
Daha sonra tekrar ikiye böleriz.
Bu işlem karakter bulunana kadar devam eder.
Dolayısıyla Blind SQL Injection yalnızca SQL bilgisi değil, aynı zamanda algoritmik düşünme gerektirir.
Bu yüzden bu tür testlerde Python gibi bir dil kullanmak oldukça mantıklıdır. HTTP isteklerini otomatik gönderip TRUE/FALSE sonucunu analiz eden küçük bir script, manuel olarak yapılması saatler sürebilecek işlemleri otomatikleştirebilir.
UNION-Based SQL Injection
Uygulama sorgunun sonucunu doğrudan sayfaya yansıtıyorsa durum daha farklıdır.
Burada UNION kullanılabilir.
UNION'ın mantığı basittir:
İki SELECT sorgusunun sonuçlarını tek bir sonuç kümesinde birleştirir.
Örneğin:
SELECT name FROM products
UNION
SELECT name FROM categories;SELECT name FROM products
UNION
SELECT name FROM categories;Burada iki sorgunun sonuçları birleştirilir.
SQL Injection açısından önemli olan nokta ise saldırganın mevcut sorguya kendi SELECT sonucunu ekleyebilmesidir.
Fakat UNION kullanabilmek için önemli bir şart vardır:
SELECT ifadelerindeki kolon sayıları uyumlu olmalıdır.
Bu nedenle ilk aşamalardan biri mevcut sorgunun kaç kolon döndürdüğünü anlamaktır.
Örneğin:
ORDER BY 1
ORDER BY 2
ORDER BY 3ORDER BY 1
ORDER BY 2
ORDER BY 3şeklinde yapılan testlerle kolon sayısı hakkında fikir edinilebilir.
Alternatif olarak UNION SELECT ile farklı kolon sayıları denenebilir.
Burada NULL değerlerinin de önemli bir avantajı vardır:
UNION SELECT NULL,NULL,NULLUNION SELECT NULL,NULL,NULLNULL, belirli bir veri tipine sıkı şekilde bağlı olmadığı için farklı kolon tipleriyle çalışırken kullanışlı olabilir.
Veritabanını Tanımak
SQL Injection başarılı olduğunda yalnızca bir tabloyu okumak zorunda değiliz.
Öncelikle hangi DBMS'in kullanıldığını anlamak önemlidir.
Örneğin:
- MySQL
- PostgreSQL
- Microsoft SQL Server
- Oracle
aynı SQL standardını temel alsalar da fonksiyonları, sistem tabloları ve bazı sözdizimleri birbirinden farklıdır.
Bu nedenle SQL Injection testinde kullanılan payload'ın veritabanına göre değişmesi normaldir.
Örneğin MySQL tarafında:
database()database()ile mevcut veritabanının adı öğrenilebilirken, farklı DBMS'lerde farklı fonksiyonlar kullanılabilir.
MySQL'de ayrıca:
information_schemainformation_schemaoldukça önemli bir kaynaktır.
Buradan tablolar ve kolonlar hakkında metadata elde edilebilir.
Örneğin tabloları listelemek için kullanılan sorgular genel olarak information_schema.tables üzerinden ilerler.
Daha sonra belirli bir tablonun kolonları information_schema.columns üzerinden incelenebilir.
Bu aşamayı bir veritabanının haritasını çıkarmaya benzetiyorum:
Database
↓
Tables
↓
Columns
↓
RecordsDatabase
↓
Tables
↓
Columns
↓
RecordsÖnemli olan doğrudan veri çekmekten önce veritabanının yapısını anlamaktır.
Time-Based Blind SQL Injection
Bazı uygulamalarda TRUE ve FALSE durumlarında sayfa tamamen aynı görünür.
HTTP status code değişmez.
Sayfanın içeriği değişmez.
Hata mesajı yoktur.
Bu durumda uygulamanın verdiği başka bir sinyali kullanmak gerekir:
Zaman.
Örneğin MySQL'de SLEEP() fonksiyonu belirli bir süre beklemeyi sağlar.
Mantık şu şekilde kurulabilir:
IF(condition, SLEEP(5), 0)IF(condition, SLEEP(5), 0)Condition doğruysa veritabanı yaklaşık 5 saniye bekler.
Yanlışsa beklemez.
Böylece uygulamanın response süresi bize TRUE/FALSE bilgisi verebilir.
Örneğin:
Normal istek → 150 ms
Koşul yanlış → 170 ms
Koşul doğru → 5.2 saniyeNormal istek → 150 ms
Koşul yanlış → 170 ms
Koşul doğru → 5.2 saniyegibi belirgin bir fark oluşuyorsa zaman tabanlı bir kanal elde etmiş oluruz.
PostgreSQL'de benzer amaçla:
pg_sleep()pg_sleep()kullanılabilir.
Microsoft SQL Server'da ise:
WAITFOR DELAYWAITFOR DELAYyaklaşımı bulunur.
Burada dikkat edilmesi gereken önemli bir nokta var: Tek bir yavaş response SQL Injection kanıtı değildir.
Ağ gecikmesi, sunucu yoğunluğu veya uygulamanın başka işlemleri de response süresini artırabilir.
Bu nedenle time-based testlerde aynı isteği birden fazla kez göndermek ve baseline oluşturmak önemlidir.
SQL Injection Her Zaman String Parametrelerinde Olmaz
SQL Injection'ın yalnızca:
username='username='gibi string alanlarında ortaya çıktığını düşünmek yanlış olur.
Sayısal parametreler de riskli olabilir.
Örneğin:
const id = req.query.id;
const query = `SELECT * FROM users WHERE id = ${id}`;const id = req.query.id;
const query = `SELECT * FROM users WHERE id = ${id}`;Burada id bir integer olarak düşünülüyor olsa bile kullanıcıdan gelen değer doğrudan SQL sorgusuna ekleniyor.
Sorun veri tipinden çok şurada:
Kullanıcı girdisi SQL syntax'ının içine doğrudan yerleştiriliyor.
Bu nedenle input validation tek başına yeterli bir güvenlik mekanizması değildir.
Bir parametrenin sayı olması bekleniyorsa gerçekten sayıya dönüştürülmesi faydalıdır:
const id = Number(req.query.id);const id = Number(req.query.id);Ancak asıl çözüm yine parametrik sorgudur.
SQL Injection ve Prepared Statements
SQL Injection'a karşı en önemli savunmalardan biri parameterized query veya prepared statement kullanmaktır.
Zafiyetli yaklaşım:
const query = `SELECT * FROM users WHERE id = ${id}`;const query = `SELECT * FROM users WHERE id = ${id}`;Güvenli yaklaşım:
const query = "SELECT * FROM users WHERE id = ?";
db.query(query, [id]);const query = "SELECT * FROM users WHERE id = ?";
db.query(query, [id]);Buradaki temel fark sadece karakterleri escape etmek değildir.
Uygulama SQL yapısını ve kullanıcı verisini birbirinden ayırır.
Örneğin kullanıcı parametre olarak:
' OR 1=1 --' OR 1=1 --gönderse bile bu değer SQL syntax'ının bir parçası haline gelmek yerine parametre olarak ele alınır.
Bu ayrım SQL Injection savunmasının temelidir:
SQL Code ≠ User DataSQL Code ≠ User DataKullanıcı girdisini SQL koduna dönüştürmemek gerekir.
ORM Kullanmak SQL Injection'ı Otomatik Olarak Çözmez
Geliştirici tarafında sık yapılan başka bir hata da ORM kullanıldığı zaman SQL Injection'ın artık mümkün olmadığını düşünmek.
Prisma, Sequelize, Entity Framework veya Hibernate gibi ORM'ler birçok SQL Injection riskini azaltabilir.
Ancak geliştirici raw SQL kullanıyorsa aynı problem tekrar ortaya çıkabilir.
Örneğin:
sequelize.query(
`SELECT * FROM users WHERE id = ${id}`
);sequelize.query(
`SELECT * FROM users WHERE id = ${id}`
);şeklinde kullanıcı girdisini sorguya doğrudan eklemek ORM kullanılmasına rağmen risklidir.
Dolayısıyla kullanılan framework değil, verinin sorguya nasıl dahil edildiği önemlidir.
SQL Injection Tespitinde Burp Suite'in Yeri
Manuel SQL Injection testlerinde Burp Suite oldukça kullanışlıdır.
Örneğin bir request:
GET /product?id=10 HTTP/1.1
Host: example.comGET /product?id=10 HTTP/1.1
Host: example.comşeklindeyse parametreyi Burp Repeater üzerinden değiştirerek farklı cevapları karşılaştırabiliriz.
Burada özellikle şu değerleri karşılaştırmak faydalıdır:
Normal değer
Bozuk syntax
TRUE koşulu
FALSE koşulu
Gecikme oluşturan koşulNormal değer
Bozuk syntax
TRUE koşulu
FALSE koşulu
Gecikme oluşturan koşulAmaç yalnızca payload denemek değildir.
Amaç uygulamanın davranışını anlamaktır.
Örneğin:
TestResponseSüreNormalÜrün bulundu180 msTRUEÜrün bulundu175 msFALSEÜrün bulunamadı170 msTime-Based TRUEÜrün bulundu5.2 s
Bu tablo tek başına exploit yazmaktan daha değerlidir. Çünkü uygulamanın nasıl tepki verdiğini gösterir.
SQL Injection'da Asıl Öğrenilmesi Gereken Şey
SQL Injection öğrenirken ezberlenebilecek yüzlerce payload var.
Fakat payload ezberlemek, SQL Injection'ı anlamakla aynı şey değil.
Asıl bilinmesi gerekenler şunlar:
1. Uygulama hangi SQL sorgusunu oluşturuyor?
2. Kullanıcı girdisi sorgunun hangi bölümüne giriyor?
3. Girdi SQL syntax'ını değiştirebiliyor mu?
4. TRUE ve FALSE durumlarında uygulama farklı davranıyor mu?
5. Veritabanından gelen sonuç kullanıcıya yansıyor mu?
6. Uygulama response süresindeki değişimleri gözlemlemeye izin veriyor mu?
7. Hangi DBMS kullanılıyor?
Bu sorular cevaplandığında hangi SQL Injection tekniğinin kullanılabileceği zaten büyük ölçüde ortaya çıkıyor.
Sonuç
SQL Injection bana göre öğrenilmesi gereken en temel web güvenliği konularından biri çünkü tek bir zafiyet üzerinden birçok farklı konuyu bir araya getiriyor.
SQL syntax'ı, HTTP istekleri, backend kodu, veritabanı mimarisi, input validation, prepared statements ve hatta otomasyon için Python gibi konular aynı problem üzerinde birleşiyor.
Özellikle laboratuvarlarda çalışırken fark ettiğim en önemli şey, SQL Injection'ın "' OR 1=1 --" gibi birkaç klasik payload'dan ibaret olmadığı oldu.
Bir uygulamayı test ederken önce onun nasıl çalıştığını anlamak, daha sonra hipotez oluşturmak ve gözlemlenen davranışları karşılaştırmak gerekiyor.
Bir SQL Injection testinin mantığını şu şekilde özetleyebilirim:
Request
↓
Input
↓
SQL Query
↓
DBMS Parsing
↓
Query Execution
↓
Application Response
↓
Response AnalysisRequest
↓
Input
↓
SQL Query
↓
DBMS Parsing
↓
Query Execution
↓
Application Response
↓
Response AnalysisZafiyet aslında bu zincirin Input → SQL Query aşamasında ortaya çıkıyor.
Kullanıcı verisi ile SQL kodu birbirinden ayrılmadığı sürece, uygulamanın veritabanına gönderdiği sorgunun mantığını kullanıcı etkileyebilir.
Bu yüzden SQL Injection'ı öğrenmenin en iyi yolu yalnızca payload ezberlemek değil; bir SQL sorgusunun backend'den veritabanına kadar nasıl oluştuğunu anlamaktır.
Bir sonraki yazımda diğer zafiyetleri anlatmaya devam edeceğim…