İçeriğe atla
0 XP 0
← Blog
Güvenli Yazılım

Python SQL Injection Önleme: Parametreli Sorgu Rehberi

Python SQL injection önleme rehberi: sqlite3, psycopg, SQLAlchemy ve Django'da parametreli sorgu, tablo adı için izin listesi ve kod inceleme adımları.

Yazan: AltaySec AkademiSon güncelleme: 6 dk okuma

Python SQL injection önleme işinin tek temel kuralı var: kullanıcıdan gelen değeri sorgu metnine f-string, % ya da + ile ekleme. Değeri execute() çağrısının ikinci argümanı olarak ver. Böylece sürücü değeri her zaman veri olarak işler, SQL komutu olarak değil. Bu yazıda sqlite3, psycopg, SQLAlchemy ve Django'da bu kuralın nasıl uygulandığını çalışır kodla göreceksin.

SQL injection'ın ne olduğunu, türlerini ve genel savunma katmanlarını SQL injection nedir yazısı anlatıyor. Bu sayfa geliştiriciye, doğrudan Python koduna odaklanır.

Python kodunda SQL injection nasıl oluşur?

Sorun dilde değil, sorgunun metin olarak dikilmesindedir. Aşağıdaki kod çalışır ve testleri geçer, ama güvenli değildir:

ad = "ayse' OR '1'='1"
sorgu = f"SELECT ad FROM kullanici WHERE ad = '{ad}'"
print(sorgu)

Çıktı şudur:

SELECT ad FROM kullanici WHERE ad = 'ayse' OR '1'='1'

Kullanıcının yazdığı tırnak, sorgunun yapısını değiştirdi. Koşul artık her satır için doğru ve tablo tümüyle dönüyor. Python'un sqlite3 belgesi de benzer bir örnek gösterir ve dize işlemleriyle sorgu kurmanın bu yüzden açığa yol açtığını söyler.

Bu tür denemeleri yalnız kendi yerel ortamında yap. Başkasının sistemine izinsiz sorgu göndermek, Türk Ceza Kanunu'nun bilişim suçlarını düzenleyen maddeleri kapsamında suç oluşturabilir.

Parametreli sorgu nedir, PEP 249 ne der?

Parametreli sorguda sorgu metni sabittir. Değerin geleceği yere bir yer tutucu konur. Değerler ayrı bir demet ya da sözlük olarak gönderilir. Python'un veritabanı arayüz standardı PEP 249 (DB-API 2.0), her sürücünün hangi yer tutucuyu kullandığını paramstyle değişkeninde bildirmesini ister:

paramstyleGörünümKullanan sürücü örneği
qmarkWHERE ad = ?sqlite3
namedWHERE ad = :adsqlite3, SQLAlchemy text()
formatWHERE ad = %spsycopg
pyformatWHERE ad = %(ad)spsycopg
numericWHERE ad = :1Daha az yaygın

Dikkat et: psycopg'deki %s bir yer tutucudur, Python'un % biçimlendirme operatörü değildir. İkisi aynı görünür ama biri güvenli, diğeri açıktır.

sqlite3 ile güvenli sorgu nasıl yazılır?

Aşağıdaki kod standart kütüphaneyle çalışır, hiçbir kurulum istemez:

import sqlite3

baglanti = sqlite3.connect(":memory:")
imlec = baglanti.cursor()
imlec.execute("CREATE TABLE kullanici (id INTEGER PRIMARY KEY, ad TEXT, eposta TEXT)")
imlec.executemany(
"INSERT INTO kullanici (ad, eposta) VALUES (?, ?)",
[("ayse", "[email protected]"), ("mehmet", "[email protected]")],
)

ad = "ayse' OR '1'='1"
imlec.execute("SELECT ad, eposta FROM kullanici WHERE ad = ?", (ad,))
print(imlec.fetchall())

imlec.execute("SELECT ad FROM kullanici WHERE ad = :ad", {"ad": "mehmet"})
print(imlec.fetchall())
```

İlk sorgu boş liste döner. Aynı girdi bu kez yalnız bir ad olarak arandı ve öyle bir kullanıcı yok. İkinci sorgu adlandırılmış yer tutucuyu gösterir. Toplu eklemede executemany() da aynı bağlama mantığını kullanır.

Tek elemanlı demette virgülü unutma. (ad,) bir demettir, (ad) ise yalnız parantez içindeki bir metindir ve sürücü hata verir.

psycopg ile PostgreSQL'de güvenli sorgu nasıl yazılır?

psycopg 3 belgesi kuralı açıkça yazar: değerleri sorguya % ya da + ile birleştirme. Yer tutucu her veri türü için %s'dir:

import psycopg

with psycopg.connect("dbname=uygulama user=uygulama_okur") as baglanti:
with baglanti.cursor() as imlec:
imlec.execute("SELECT eposta FROM kullanici WHERE ad = %s", (ad,))
imlec.execute("SELECT eposta FROM kullanici WHERE ad = %(ad)s", {"ad": ad})
```

psycopg 3'te değerler sorguyla birleştirilmez, sorgu ve parametreler sunucuya ayrı gönderilir. Tablo ya da sütun adı dinamikse psycopg.sql modülünü kullan:

from psycopg import sql

tablo = "kullanici"
imlec.execute(
sql.SQL("SELECT ad FROM {} WHERE id = %s").format(sql.Identifier(tablo)),
(42,),
)
```

sql.Identifier adı doğru biçimde alıntılar. Yine de tablo adını kullanıcıdan alıyorsan önce bir izin listesinden geçir.

SQLAlchemy ve Django'da ham sorgu nasıl güvenli kalır?

İki çatı da filtre yöntemlerinde değerleri kendisi bağlar. Risk, geliştiricinin ham SQL'e indiği yerdedir.

SQLAlchemy'de ham sorgu text() ile yazılır ve değer iki nokta ile adlandırılmış parametre olarak verilir:

from sqlalchemy import create_engine, text

motor = create_engine("sqlite://")
with motor.connect() as baglanti:
sonuc = baglanti.execute(
text("SELECT eposta FROM kullanici WHERE ad = :ad"),
{"ad": ad},
)
```

SQLAlchemy belgesi, metin SQL'de bir Python değerinin sayı ya da tarih bile olsa sorguya doğrudan yazılmamasını, her zaman parametre kullanılmasını söyler.

Django'da raw() yöntemi değerleri params argümanıyla alır:

Kisi.objects.raw("SELECT * FROM uygulama_kisi WHERE soyad = %s", [soyad])

Django belgesi iki hatayı ayrıca uyarır: sorguyu % ile doldurmak ve yer tutucuyu '%s' biçiminde tırnak içine almak. İkisi de korumayı bozar.

Tablo adı, sıralama ve IN listesi nasıl güvenle kurulur?

Parametreler yalnız değerleri bağlar. Sorgunun yapısal parçaları için başka yöntemler gerekir:

Dinamik parçaDoğru yöntem
Değer (WHERE ad = ...)Yer tutucu ve parametre
Sütun adı, ORDER BY alanıSabit sözlükle izin listesi
Tablo adıİzin listesi, PostgreSQL'de ayrıca sql.Identifier
IN (...) listesiDeğer sayısı kadar yer tutucu
LIKE aramasıJoker karakteri değerin içine koy, değeri bağla

Sıralama alanı için izin listesi:

SIRALAMA = {"ad": "ad", "eposta": "eposta"}

def kullanicilari_listele(imlec, siralama):
sutun = SIRALAMA.get(siralama)
if sutun is None:
raise ValueError("Geçersiz sıralama alanı")
return imlec.execute(f"SELECT ad FROM kullanici ORDER BY {sutun}").fetchall()
```

Burada f-string güvenlidir, çünkü sorguya giren değer kullanıcının yazdığı metin değil, sözlükteki sabit addır. IN listesi ve LIKE araması için:

kimlikler = [1, 2]
yer_tutucular = ", ".join("?" for _ in kimlikler)
imlec.execute(f"SELECT ad FROM kullanici WHERE id IN ({yer_tutucular})", kimlikler)

arama = "ay"
imlec.execute("SELECT ad FROM kullanici WHERE ad LIKE ?", (f"%{arama}%",))
```

Kod tarayıcıları bu satırlardaki f-string'i işaretleyebilir. İnceleme notuna neden güvenli olduğunu yaz.

Python SQL injection önlemede hangi ek katmanlar gerekir?

Parametreli sorgu ana savunmadır. Şu katmanlar, bir hata gözden kaçarsa zararı sınırlar:

  • İzin listesiyle girdi doğrulama: Sayı beklenen alanda sayı, belirli biçim beklenen alanda o biçim kabul et. Uymayan girdiyi düzeltmeye çalışma, reddet.
  • En az yetkili veritabanı hesabı: Uygulamanın hesabı tablo silmemeli, şema değiştirmemeli. Yönetim işleri için ayrı hesap kullan.
  • Hata mesajlarını gizlemek: Kullanıcıya genel bir mesaj göster. Tablo ve sütun adı içeren ayrıntı yalnız sunucu günlüğüne gitsin.
  • Gizli bilgileri koddan çıkarmak: Bağlantı parolasını kaynak koda yazma, ortam değişkeninden ya da sır yönetiminden oku.

Bu katmanların web uygulamasının geneline nasıl oturduğunu OWASP Top 10 yazısındaki enjeksiyon başlığında görebilirsin.

Kodunda SQL injection'ı nasıl yakalarsın?

İnceleme ve test için üç adım yeterli bir başlangıçtır:

  1. Ara: Depoda execute(f", execute("..." % ve + ile kurulan sorguları ara. text(f" ve raw(f" kalıplarını da listeye ekle.
  2. Otomatik tara: Bandit'in B608 kuralı, dize işlemleriyle kurulan SQL ifadelerini olası enjeksiyon olarak işaretler. Bandit'i sürekli entegrasyon hattına ekle. Bu yaklaşımın tamamını DevSecOps nedir yazısında bulabilirsin.
  3. Test et: Kendi uygulamanın birim testlerine tırnak, noktalı virgül ve yorum işareti içeren girdiler ekle. Sonuç, bu girdinin düz metin olarak arandığını göstermeli.

Python'a yeni başlıyorsan önce siber güvenlik için Python yazısıyla temeli kur. Güvenli kod incelemesini uygulamalı denemek için laboratuvar bölümünde bir kod incelemesinde SQL'e karışan kullanıcı girdisini satır satır bulduğun bir senaryo var. Konuyu baştan sona çalışmak istersen Güvenli Kod yolunu izle.

Kaynaklar

Sık sorulan sorular

ORM kullanırsam SQL injection riski biter mi?

Büyük ölçüde azalır ama bitmez. SQLAlchemy ve Django ORM, filtre yöntemlerinde değerleri kendisi bağlar. Risk, ham SQL çalıştıran yöntemlerde geri gelir: SQLAlchemy'de text(), Django'da raw() ve cursor.execute(). Bu yöntemlere kullanıcı girdisini f-string ile eklersen açık oluşur. Ham sorguda da değeri her zaman parametre olarak ver.

Python'da %s yer tutucusu kullanmak güvenli mi?

Duruma göre değişir. psycopg gibi sürücülerde %s bir yer tutucudur ve değer execute çağrısının ikinci argümanıyla ayrı gönderilirse güvenlidir. Aynı sorguyu Python'un % operatörüyle doldurursan değer metne yapışır ve korumanın tamamı kaybolur. Kural basit: sorgu metnine hiçbir zaman % ya da + ile değer ekleme.

Tablo ya da sütun adını parametre olarak verebilir miyim?

Hayır. Parametreler yalnız değerleri bağlar, tablo ve sütun adları gibi tanımlayıcıları bağlamaz. Kullanıcı seçimine göre sıralama ya da tablo değişiyorsa izin verilen adları sabit bir sözlükte tut ve gelen değeri bu sözlükle eşle. PostgreSQL ile psycopg kullanıyorsan sql.Identifier sınıfı tanımlayıcıyı güvenli biçimde alıntılar.

Kullanıcı girdisini temizlemek (escape) yeterli olur mu?

Ana savunma olarak yeterli değildir. Elle kaçış kodun bir yerinde unutulur, veritabanına göre farklı kurallar ister ve sayı gibi tırnaksız alanlarda işe yaramaz. Parametreli sorgu veriyi komuttan baştan ayırır. Girdi doğrulamayı ek katman olarak kullan: beklenen tür, uzunluk ve biçim dışındaki değeri reddet.

IN listesine birden çok değer güvenle nasıl verilir?

Değer sayısı kadar yer tutucu üret ve değerleri ayrı bağla. sqlite3'te üç kimlik için sorguya IN (?, ?, ?) yazılır, liste execute çağrısının ikinci argümanı olur. Yer tutucu dizisini kodla oluşturmak güvenlidir, çünkü metne yalnız soru işaretleri girer. psycopg'de ise listeyi doğrudan = ANY(%s) biçiminde verebilirsin.

  • Python
  • SQL injection
  • parametreli sorgu
  • güvenli kodlama
  • SQLAlchemy

İlgili yazılar

Okuduğunu uygula.
Yollarda ders ders ilerle, alıştırmalarla pekiştir.
Başla