Skip to content
Siber Vatan

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 ​

ParametreDeğer
CVE IDCVE-2022-34265
Zafiyet TürüSQL Injection (CWE-89)
CVSS v3 SkoruNVD: 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 PaketDjango
Etkilenen SürümlerDjango 4.1 (ana dal ve beta), Django 4.0 (< 4.0.6), Django 3.2 (< 3.2.14)
Giderilen Sürümler3.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. kind parametresi ile hedeflenen zaman dilimini belirler.

Django Trunc Dokümantasyonu

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

Django Extract Dokümantasyonu

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? ​

  1. 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.
  2. 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.
  3. Geniş Çaplı Veri Sızıntısı ve Manipülasyon: Kullanılan veritabanı motoruna (PostgreSQL, SQLite, MySQL vb.) bağlı olarak; saldırgan UNION tabanlı 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.

python
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:

http
GET /report?lookup_name=year HTTP/1.1

Arka planda PostgreSQL üzerinde çalışan SQL sorgusu:

sql
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:

http
GET /report?lookup_name=year%27%20FROM%20start_datetime))%20OR%201=1;SELECT%20PG_SLEEP(5)-- HTTP/1.1

Django, 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:

sql
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.
  • kind veya lookup_name argü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.

bash
pip install --upgrade django

Django ekibinin eklediği kontrolün kavramsal karşılığı şudur:

python
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.

python
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.


Kaynakça ​

CVE-2022-34265 İçin Kaynakça