Django'da SQL Injection: CVE-2022-34265 Teknik Analizi
Saide Beyza Bozyiğit · @0beyz64
Muhammed Hüseyin Özkaya · @mhuseyinozkaya
Yusuf Öz · @Yusuf0Z
Web güvenliğinde modern çatılar (frameworks), sağladıkları ORM (Object-Relational Mapping) katmanı sayesinde ham SQL sorguları yazma ihtiyacını ortadan kaldırır ve SQL Injection riskini büyük oranda en aza indirir. Ancak güvenlik açıkları yalnızca uygulama seviyesinde değil, bizzat çatının kendi çekirdek bileşenlerinde de ortaya çıkabilir.
Django ekosisteminde keşfedilen CVE-2022-34265, ORM mimarisine duyulan mutlak güvenin neden sorgulanması gerektiğini ortaya koyan kritik bir zafiyettir.
Uyarı
ORM fonksiyonlarının arka planda parametrik sorgu kullanmadığı veya girdi doğrulaması yapmadığı senaryolarda SQL Injection açıkları framework seviyesinde tetiklenebilir.
Zafiyetin Özeti
| Parametre | Değer |
|---|---|
| CVE ID | CVE-2022-34265 |
| Zafiyet Türü | SQL Injection (CWE-89) |
| CVSS v3 Skoru | NVD: 9.8 (Critical) / Django: High |
| CVSS Vektörü | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Etkilenen Paket | Django |
| Etkilenen Sürümler | Django 4.1 (ana dal ve beta), Django 4.0 (< 4.0.6), Django 3.2 (< 3.2.14) |
| Giderilen Sürümler | 3.2.14, 4.0.6 ve 4.1 beta dalı yamaları |
Önemli Not
Bu zafiyet yalnızca uygulamanın kind / lookup_name değerini doğrudan kullanıcı girdisinden aldığı durumlarda tetiklenir. Django’nun resmi bildirimine göre, bu değerleri sabit bir güvenli listeyle sınırlayan uygulamalar etkilenmez.
NVD’ye göre CVSS 9.8 (Critical); Django kendi güvenlik politikasında bu zafiyeti "high" seviyesinde derecelendirmiştir.
Zafiyet; Django'nun veritabanı fonksiyonları arasında yer alan Trunc() ve Extract() sınıflarında kullanıcıdan alınan girdilerin filtrelenmeden doğrudan SQL sorgusuna dize (string) olarak eklenmesinden kaynaklanmaktadır. Bu durum, uzaktaki yetkisiz bir saldırganın arka plandaki veritabanında rastgele SQL komutları çalıştırmasına olanak tanır.
Bu Zafiyetin Temel Nedeni
Django ORM, tarih ve zaman verileri üzerinde hesaplama veya kırılım yapabilmek amacıyla django.db.models.functions modülü altında Trunc() ve Extract() fonksiyonlarını barındırır.
Trunc(): Bir tarih/saat damgasını belirli bir aralığa (yıl, ay, gün vb.) yuvarlar.kindparametresi ile hedeflenen zaman dilimini belirler.

Extract(): Bir tarih/saat alanından belirli bir bileşeni (yalnızca ay, gün veya dakika gibi) çeker.lookup_nameargümanı ile çekilecek parçayı tanımlar.

Zafiyetin temel nedeni, Django'nun bu parametreleri veritabanına iletirken parametrik sorgulama (parameter binding) kullanmaması ve bu değerleri kabul edilebilir girdiler listesinden (allow-list) geçirmemesidir. Argüman olarak verilen değer doğrudan SQL dizesine formatlandığı için, saldırgan tarafından sağlanan tırnak işaretleri ve zararlı ifadeler SQL bağlamından çıkabilmektedir.
Bu Zafiyet Neden Önemli?
- ORM Korumasının Çökmesi: Geliştiriciler Django ORM kullandıkları sürece SQL Injection'a karşı korundukları varsayımıyla hareket eder. Bu açık, ORM'in bizzat kendisinin bir saldırı vektörü haline gelebileceğini göstermiştir.
- Kimlik Doğrulama Gerektirmemesi: İlgili parametre halka açık bir arama veya filtreleme ucuna (endpoint) bağlıysa, sisteme giriş yapmamış saldırganlar tarafından doğrudan tetiklenebilir.
- Geniş Çaplı Veri Sızıntısı ve Manipülasyon: Kullanılan veritabanı motoruna (PostgreSQL, SQLite, MySQL vb.) bağlı olarak; saldırgan
UNIONtabanlı sorgularla hassas verileri sızdırabilir, kayıtları değiştirebilir veya yetki yükseltebilir.
Zafiyet Nasıl Oluşuyor?
Bir analitik veya raporlama ekranında kullanıcıların verileri farklı zaman dilimlerine göre filtrelemesine izin veren yaygın bir senaryoyu ele alalım.
Aşağıdaki örnek PostgreSQL sözdizimine göredir; enjeksiyonun tam biçimi veritabanı motoruna göre değişir.
from django.shortcuts import render
from django.db.models.functions import Extract
from .models import Experiment
def experiment_report(request):
lookup = request.GET.get('lookup_name', 'year')
report_data = Experiment.objects.annotate(
extracted=Extract('start_datetime', lookup_name=lookup)
).values('extracted')
return render(request, 'report.html', {'data': report_data})Normal şartlarda istek şu şekilde yapılır:
GET /report?lookup_name=year HTTP/1.1Arka planda PostgreSQL üzerinde çalışan SQL sorgusu:
SELECT EXTRACT('year' FROM "start_datetime") AS "extracted" FROM "experiment";Ancak saldırgan parametreyi zaman tabanlı (time-based) bir SQL Injection yüküyle manipüle ettiğinde:
GET /report?lookup_name=year%27%20FROM%20start_datetime))%20OR%201=1;SELECT%20PG_SLEEP(5)-- HTTP/1.1Django, girdiyi parametrik bağlama yerine doğrudan dize birleştirme ile SQL sorgusuna eklediği için tırnak işareti kapanır, parantez dengesi tamamlanır ve saldırganın eklediği SQL komutu icra edilir:
SELECT EXTRACT('year' FROM start_datetime)) OR 1=1;SELECT PG_SLEEP(5)-- ...Bu noktada veritabanı motoru PG_SLEEP(5) çağrısını işleterek yanıtı 5 saniye geciktirir. Saldırgan bu zaman tabanlı teyit mekanizmasını kullanarak veritabanından karakter karakter veri sızdırabilir.
Zafiyet Nasıl Gözden Kaçtı?
SQL parametreleştirmesinde WHERE veya VALUES bloklarındaki dinamik değerler kolayca %s veya ? gibi bağlayıcılarla veritabanı sürücüsüne devredilebilir. Ancak SQL standardında DATE_TRUNC(unit, column) veya EXTRACT(unit FROM column) fonksiyonlarındaki unit (zaman birimi) parametresi bir veri değeri değil, bir SQL anahtar kelimesi/tanımlayıcısı (identifier) muamelesi görür.
Veritabanı motorlarının çoğu, zaman dilimi belirteçlerinin sorguda bind parametresi olarak kullanılmasına izin vermez; bu belirteçlerin doğrudan sorgu metninde yer almasını bekler.
Bu teknik kısıt nedeniyle:
- Django geliştiricileri, parametre bağlama yerine dize formatlamaya başvurdu.
kindveyalookup_nameargümanlarının yalnızca geliştiriciler tarafından tanımlanmış sabit dizeler ("year","month"gibi) olacağı varsayıldı.- Bu değerlerin doğrudan son kullanıcıdan gelme ihtimali ve kütüphane seviyesinde bir sınırlandırma getirilmesi gerektiği gözden kaçırıldı.
Bilgi
SQL tanımlayıcıları (identifier) standart parametrik sorgulama ile korunamadığı durumlarda, en güvenli yaklaşım uygulama katmanında katı izin listesi (allow-list) uygulamaktır.
Savunma Yöntemleri
1. Framework Güncellemesi
Zafiyetin kesin çözümü, güvenlik yamalarını içeren Django sürümlerine geçiş yapmaktır.
pip install --upgrade djangoDjango ekibinin eklediği kontrolün kavramsal karşılığı şudur:
allowed_lookups = {
'year', 'iso_year', 'quarter', 'month', 'week',
'week_day', 'iso_week_day', 'day', 'hour',
'minute', 'second'
}
if lookup_name.lower() not in allowed_lookups:
raise ValueError(f"Geçersiz lookup_name/kind parametresi: '{lookup_name}'")2. Uygulama Düzeyinde Katı Doğrulama
Dış kaynaklardan gelen girdiler ORM fonksiyonlarına doğrudan aktarılmamalıdır.
ALLOWED_LOOKUPS = {'year', 'month', 'day'}
lookup = request.GET.get('lookup_name', 'year')
if lookup not in ALLOWED_LOOKUPS:
lookup = 'year'
report_data = Experiment.objects.annotate(
extracted=Extract('start_datetime', lookup_name=lookup)
).values('extracted')İpucu
Kullanıcıdan gelen parametreleri karşılamak için Django formlarını (forms.ChoiceField) kullanmak, izin verilmeyen girdileri görünüm (view) katmanına ulaşmadan engeller.

