İçeriğe geç
academia.sh

Ders 03 / 17

Enjeksiyon Savunması

Sorgu noktalarında kapsamanın ölçülmesi: on altı sorgu noktası aynı değer kümesiyle koşturulup değerin sorgu metnine ulaştığı nokta sayılır, parametreli sorgu arayüzünün yapısal olarak kapattığı yer ile tanımlayıcı yerinin neden kapatılamadığı ayrılır ve dört yeni sorgu eklendiğinde katı katmanla esnek katmanın kapsaması karşılaştırılır.

İçindekiler

Önceki ders değerin okunacağı kabı hedef aldı: gövde, öznitelik, betik, adres. Dördünde de kap bir belgeydi ve okuyan taraf bir görüntüleyiciydi. Değerin bir sorgu metnine konması farklı bir sorundur, çünkü orada okuyan taraf veriyle komutu ayırmak zorundadır ve ayrımı yapacağı tek şey metnin kendisidir.

Bu ders sorgu noktalarını sayar. Sunucu Güvenliği ve Üretime Alma kursu iki kurulumun ürettiği sonuç farkını ölçmüştü; burada ölçülen şey sonuç değil kapsamadır: kaç sorgu değeri metnin dışında tutuyor, kaçı metne katıyor ve yeni bir sorgu yazıldığında hangisi noktayı kendiliğinden kapsıyor.

Sorgu Metnine Ulaşan Değer

Ölçü tek sorudur: sorgu metni kurulduğunda değerin kendisi o metinde görünüyor mu. Bu soru bir veritabanı motoru gerektirmez; metin bir dizgedir ve değer o dizgede ya vardır ya yoktur.

GC7 (varsayım): sorgu metni modeldir, çalıştıran motor yoktur. Bir nokta, değerlerinden hiçbiri metinde görünmüyorsa kapsanmış sayılır. Motorun metni nasıl ayrıştırdığı ölçülmez.

// arayuz.mjs — iki sorgu olusturma bicimi. Sorgu metni model, calistiran motor yok.
// Olculen sey tek sorudur: deger sorgu metnine ulasti mi?

// Parametreli sorgu arayuzu: yalniz etiketli sablon kabul eder; dizge kabul etmez.
export function sor(parcalar, ...degerler) {
  if (!Array.isArray(parcalar?.raw)) throw new TypeError("sorgu metni dizge olarak verilemez");
  let metin = parcalar[0];
  const islenen = [];
  degerler.forEach((d, i) => {
    if (d && d.tanimlayici !== undefined) {
      if (d.tanimlayici === null) throw new RangeError("izin listesinde olmayan tanimlayici");
      metin += d.tanimlayici;                    // yer degil, ad: islenen olarak gecirilemez
    } else { metin += "?"; islenen.push(d); }
    metin += parcalar[i + 1];
  });
  return { metin, islenen, denetimli: true };
}

// Elle birlestirme: her sey metne katilir, islenen kalmaz.
export function elle(parcalar, ...degerler) {
  let metin = parcalar[0];
  degerler.forEach((d, i) => {
    metin += String(d && d.tanimlayici !== undefined ? d.ham : d) + parcalar[i + 1];
  });
  return { metin, islenen: [], denetimli: false };
}

// Tanimlayici yeri: alan ve tablo adlari icin izin listesi.
const ALAN = { okuma_zamani: "okuma_zamani", donem: "donem", tutar: "tutar" };
export const ad = (x) => ({ tanimlayici: ALAN[x] ?? null, ham: x });

// Sizinti denetcisi: verilen degerlerden kaci sorgu metninde gorunuyor.
export const sizinti = (s, degerler) =>
  degerler.filter((d) => s.metin.includes(String(d))).length;

İki biçimin yazılışı neredeyse aynıdır — ikisi de etiketli şablon alır, ikisi de aynı sorguyu kurar. Ayrıştıkları tek yer değerin nereye gittiğidir: biri metne yer tutucu koyup değeri ayrı bir diziye yazar, öteki değeri metnin içine katar.

Sorgu Noktası Envanteri

Bölgesel ölçüm ağının sorgu noktaları sayılıyor: abone, faturalama, okuma, iş emri ve rapor servisleri. Her nokta bir sorgu şeklini bir arayüzle yazar.

// kapsama.mjs — sorgu noktasi envanteri (kurgu model) ve iki bicimin kapsamasi
import { sor, elle, ad, sizinti } from "./arayuz.mjs";

// Dort sorgu sekli; her sekil ayni degerleri alir, tek fark hangi arayuzle yazildigi.
const SEKIL = {
  ara:    (b, v) => b`SELECT no FROM abone WHERE bolge = ${v.bolge} AND tarife = ${v.tarife}`,
  getir:  (b, v) => b`SELECT * FROM fatura WHERE abone = ${v.abone} AND donem = ${v.donem}`,
  sirala: (b, v) => b`SELECT * FROM okuma ORDER BY ${ad(v.alan)} LIMIT ${v.limit}`,
  say:    (b, v) => b`SELECT count(*) FROM is_emri WHERE tur = ${v.tur}`,
};
const V = { bolge: "kuzey", tarife: "mesken", abone: "A-100432", donem: "2026-03",
  alan: "okuma_zamani", limit: 50, tur: "kesme" };
const DEGERI = { ara: ["kuzey", "mesken"], getir: ["A-100432", "2026-03"], sirala: [50], say: ["kesme"] };

// [servis, sorgu noktasi, sekil, arayuz]
const NOKTA = [
  ["abone", "bolgeAra", "ara", sor], ["abone", "tarifeAra", "ara", sor],
  ["abone", "gecmisAra", "ara", elle], ["fatura", "donemGetir", "getir", sor],
  ["fatura", "borcGetir", "getir", sor], ["fatura", "arsivGetir", "getir", elle],
  ["okuma", "sonOkuma", "sirala", sor], ["okuma", "sayacGecmisi", "sirala", sor],
  ["okuma", "toplamaRaporu", "sirala", elle], ["okuma", "aykiriOkuma", "ara", sor],
  ["is-emri", "acikSay", "say", sor], ["is-emri", "bolgeSay", "say", sor],
  ["is-emri", "gecmisSay", "say", elle], ["rapor", "ozetGetir", "getir", sor],
  ["rapor", "dokumSirala", "sirala", elle], ["rapor", "aylikSay", "say", sor],
];

const olc = (liste) => {
  let ulasan = 0, tanimlayiciYeri = 0, denetimsiz = 0;
  for (const [, , sekil, b] of liste) {
    const s = SEKIL[sekil](b, V);
    ulasan += sizinti(s, DEGERI[sekil]);
    if (sekil === "sirala") { tanimlayiciYeri++; if (!s.denetimli) denetimsiz++; }
  }
  return [ulasan, tanimlayiciYeri, denetimsiz];
};

const P = NOKTA.filter((n) => n[3] === sor), E = NOKTA.filter((n) => n[3] === elle);
console.log(`${NOKTA.length} sorgu noktasi, ${Object.keys(SEKIL).length} sorgu sekli\n`);
console.log(`${"bicim".padEnd(22)}${"nokta".padStart(6)}${"deger yeri".padStart(11)}` +
  `${"metne ulasan".padStart(13)}${"tanimlayici yeri".padStart(17)}${"denetimsiz".padStart(11)}`);
for (const [adi, l] of [["parametreli arayuz", P], ["elle birlestirme", E]]) {
  const yer = l.reduce((t, n) => t + DEGERI[n[2]].length, 0);
  const [u, t, d] = olc(l);
  console.log(adi.padEnd(22) + String(l.length).padStart(6) + String(yer).padStart(11) +
    String(u).padStart(13) + String(t).padStart(17) + String(d).padStart(11));
}
console.log("\nornek metin (parametreli): " + SEKIL.getir(sor, V).metin);
console.log("ornek metin (elle)       : " + SEKIL.getir(elle, V).metin);
16 sorgu noktasi, 4 sorgu sekli

bicim                  nokta deger yeri metne ulasan tanimlayici yeri denetimsiz
parametreli arayuz        11         17            0                2          0
elle birlestirme           5          7            7                2          2

ornek metin (parametreli): SELECT * FROM fatura WHERE abone = ? AND donem = ?
ornek metin (elle)       : SELECT * FROM fatura WHERE abone = A-100432 AND donem = 2026-03

On altı noktanın on biri parametreli arayüzden geçiyor ve o on birde on yedi değer yerinin hiçbiri metne ulaşmıyor. Beş nokta elle birleştiriyor ve orada yedi değer yerinin yedisi de metnin içinde. Dışarıda kalan nokta sayısı beştir.

Bu beşin ortak bir özelliği var: üçü arşiv ve geçmiş sorgusu, ikisi rapor. Ana akış korunmuş, kenar akışlar korunmamış — çünkü karar nokta başına verilmiş ve kenar akışları yazan kişi başka bir kişi ya da başka bir zaman.

Aynı ölçü komut noktaları için de geçerlidir: değer alt sürece ayrı bir argüman olarak mı geçiyor, yoksa tek bir komut dizgesine mi katılıyor. Soru aynı sorudur; ölçüsü de aynıdır — değer metne ulaştı mı.

Sayının nasıl elde edildiği de kapsamanın bir parçasıdır. Sızıntı denetçisi burada metni tarıyor, ama arayüzün döndürdüğü denetimli bayrağı aynı bilgiyi taramadan veriyor: her sorgu sonucu hangi biçimle kurulduğunu kendisi taşıyor. Kapsamanın sayılabilir olması, savunmanın kendisinin sayı üretmesine bağlıdır — dışarıdan bakan bir tarayıcı sorgu metnini görür, o metnin nasıl kurulduğunu görmez.

Kapatılamayan Yer

Tabloda parametreli arayüzün sıfırlamadığı tek sütun tanimlayici yeri. İki nokta bir alan adını sorgunun içine koyuyor ve bir alan adı yer tutucuyla geçirilemez: yer tutucu bir değeri temsil eder, sorgunun yapısını değil. Sıralama alanı, tablo adı ve yön sözcüğü metnin kendisine yazılmak zorundadır.

Parametreli arayüz burada kapsama sağlayamaz; sağladığı tek şey bu yerin görünür olmasıdır. Kod ad() çağrısıyla işaretlendiği için tanımlayıcı yerleri sayılabilir hâle gelir ve orada tek savunma ilk dersin kalıbıdır: izin listesi. Aşağıdaki blok üç kararı yan yana koyuyor.

// tanimlayici.mjs — tanimlayici yerinde uc karar: elle, izin listesi, yer tutucu denemesi
import { sor, elle, ad } from "./arayuz.mjs";

const GIRDI = ["okuma_zamani", "donem", "tutar", "abone_no", "okuma_zamani desc", "ic_not"];
const kuyruk = (f) => { try { return f().metin.split("ORDER BY ")[1]; } catch (e) { return e.constructor.name; } };

console.log(`${GIRDI.length} siralama alani girdisi, tek tanimlayici yeri\n`);
console.log(`${"girdi".padEnd(20)}${"elle".padStart(20)}${"izin listesi".padStart(14)}${"yer tutucu".padStart(12)}`);
let ham = 0, kabul = 0, bozuk = 0;
for (const g of GIRDI) {
  const e = kuyruk(() => elle`SELECT * FROM okuma ORDER BY ${ad(g)}`);
  const i = kuyruk(() => sor`SELECT * FROM okuma ORDER BY ${ad(g)}`);
  const y = kuyruk(() => sor`SELECT * FROM okuma ORDER BY ${g}`);
  if (e === g) ham++;
  if (i === g) kabul++;
  if (y !== g) bozuk++;
  console.log(g.padEnd(20) + e.padStart(20) + i.padStart(14) + y.padStart(12));
}
console.log(`\nelle: ${ham}/${GIRDI.length} girdi metne oldugu gibi girdi`);
console.log(`izin listesi: ${kabul} kabul, ${GIRDI.length - kabul} ret`);
console.log(`yer tutucu ile gecirme: ${bozuk}/${GIRDI.length} sorgunun yapisi bozuldu`);
6 siralama alani girdisi, tek tanimlayici yeri

girdi                               elle  izin listesi  yer tutucu
okuma_zamani                okuma_zamani  okuma_zamani           ?
donem                              donem         donem           ?
tutar                              tutar         tutar           ?
abone_no                        abone_no    RangeError           ?
okuma_zamani desc      okuma_zamani desc    RangeError           ?
ic_not                            ic_not    RangeError           ?

elle: 6/6 girdi metne oldugu gibi girdi
izin listesi: 3 kabul, 3 ret
yer tutucu ile gecirme: 6/6 sorgunun yapisi bozuldu

Son sütun kapatılamazlığın kanıtıdır: girdi bir değer gibi geçirildiğinde altı sorgunun altısında da metin ORDER BY ? oluyor ve sıralama alanı sorgudan siliniyor. Yer tutucu bu yerde savunma değil, hatadır.

Ortadaki sütun tek çözümü veriyor. Altı girdinin üçü izin listesinde ve olduğu gibi geçiyor; üçü reddediliyor — biri şemada olmayan bir alan, biri yön sözcüğü eklenmiş bir alan, biri listede bulunmayan bir ad. Elle birleştirmede altısı da sorguya giriyor. Tanımlayıcı yerinin kapsaması parametreli arayüzden değil, izin listesinin uzunluğundan gelir, ve listede olmayan meşru bir alan da reddedilir — bu yerin bedeli budur.

Yeni Sorgu Eklendiğinde

Kapsamanın sınavı yine envanter büyüdüğünde başlar. Dört yeni sorgu noktası açılıyor ve iki ayrı erişim katmanında yazılıyor: biri yalnız parametreli arayüzü sunuyor, öteki sorgu metnini dizge olarak da kabul ediyor.

GC8 (varsayım): esnek katmanda yeni sorguyu yazan kişinin hangi biçimi seçeceği bilinmez; ölçülen şey katmanın izin verdiği en kötü durumdur. Katı katmanda böyle bir durum yoktur, çünkü seçenek yoktur.

// yeni-sorgu.mjs — yeni sorgu eklendiginde hangi bicim noktayi kendiliginden kapsiyor
import { sor, elle, ad, sizinti } from "./arayuz.mjs";

const dene = (f) => { try { return "gecti -> " + f().metin; } catch (e) { return e.constructor.name + ": " + e.message; } };
console.log("dizge olarak verilen sorgu   : " + dene(() => sor("SELECT no FROM abone WHERE bolge = 'kuzey'")));
console.log("izin listesi disi tanimlayici: " + dene(() => sor`SELECT * FROM okuma ORDER BY ${ad("ic_not")}`));
console.log("ayni tanimlayici, elle       : " + dene(() => elle`SELECT * FROM okuma ORDER BY ${ad("ic_not")}`));

// Dort yeni sorgu noktasi; ayni sekiller, yalniz arayuz degisiyor.
const YENI = [
  ["eksikOkuma", (b, v) => b`SELECT no FROM okuma WHERE kaynak = ${v.kaynak}`, ["saha"]],
  ["borcluAra", (b, v) => b`SELECT abone FROM fatura WHERE tutar > ${v.tutar}`, [1200]],
  ["isEmriFiltre", (b, v) => b`SELECT * FROM is_emri WHERE tur = ${v.tur} AND bolge = ${v.bolge}`, ["acma", "guney"]],
  ["gunlukDokum", (b, v) => b`SELECT * FROM okuma ORDER BY ${ad(v.alan)} LIMIT ${v.limit}`, [100]],
];
const V = { kaynak: "saha", tutar: 1200, tur: "acma", bolge: "guney", alan: "donem", limit: 100 };
const yer = YENI.reduce((t, [, , d]) => t + d.length, 0);

console.log(`\n${YENI.length} yeni sorgu noktasi, ${yer} deger yeri\n`);
console.log(`${"katman bicimi".padEnd(34)}${"metne ulasan".padStart(13)}${"denetimsiz nokta".padStart(18)}`);
for (const [baslik, b] of [["kati katman (yalniz parametreli)", sor], ["esnek katman (dizge de kabul)", elle]]) {
  const ulasan = YENI.reduce((t, [, f, d]) => t + sizinti(f(b, V), d), 0);
  const denetimsiz = YENI.filter(([, f]) => !f(b, V).denetimli).length;
  console.log(baslik.padEnd(34) + String(ulasan).padStart(13) + String(denetimsiz).padStart(18));
}
dizge olarak verilen sorgu   : TypeError: sorgu metni dizge olarak verilemez
izin listesi disi tanimlayici: RangeError: izin listesinde olmayan tanimlayici
ayni tanimlayici, elle       : gecti -> SELECT * FROM okuma ORDER BY ic_not

4 yeni sorgu noktasi, 5 deger yeri

katman bicimi                      metne ulasan  denetimsiz nokta
kati katman (yalniz parametreli)              0                 0
esnek katman (dizge de kabul)                 5                 4

İlk üç satır arayüzün kendi sınırlarını gösteriyor. Sorgu metni dizge olarak verilemez; verilirse çağrı ilk satırda durur. İzin listesinde olmayan bir alan adı da geçmez. Aynı alan adı elle birleştirmede sorgunun içine giriyor ve hiçbir belirti vermiyor.

Alt tablo asıl sayıyı veriyor. Katı katmanda dört yeni sorgunun beş değer yerinden hiçbiri metne ulaşmıyor ve bunun nedeni dikkat değil, başka bir yol olmaması. Esnek katmanda beş değer yerinin beşi de metne ulaşabiliyor; katman bunu engellemiyor, yalnız izin veriyor.

Fark burada bir alışkanlık farkı değildir. Katı katmanda kapsama kod yazılmadan sabittir; esnek katmanda her yeni sorgu, doğru biçimi seçen bir kişiye bağlıdır ve on altı noktalık envanterde bu kişi beş kez başka bir şey seçti.

Özet

  • Ölçü tek sorudur: sorgu metni kurulduğunda değer o metinde görünüyor mu. Bu soru veritabanı motoru gerektirmez; sorgu metni bir dizgedir.
  • On altı sorgu noktasının 11’i parametreli arayüzden geçiyor ve orada 17 değer yerinin 0’ı metne ulaşıyor; 5 nokta elle birleştiriyor ve orada 7 değer yerinin 7’si metnin içinde. Dışarıda kalan nokta sayısı 5’tir ve beşi de kenar akıştır.
  • Parametreli arayüzün kapatamadığı bir yer var: tanımlayıcı yeri. Alan adı bir yer tutucuyla geçirilemez, çünkü yer tutucu değeri temsil eder, yapıyı değil. Envanterde 2 böyle nokta var. Altı sıralama girdisi değer gibi geçirildiğinde 6 sorgunun yapısı bozuldu; izin listesi 3’ünü kabul, 3’ünü ret etti; elle birleştirmede 6’sı da sorguya girdi.
  • Arayüz sorgu metnini dizge olarak kabul etmiyor ve izin listesi dışı tanımlayıcıyı reddediyor; ikisi de çağrı anında durur. Elle birleştirmede aynı tanımlayıcı sorguya girer ve hiçbir belirti vermez.
  • Dört yeni sorguda katı katman 0 değer yeri sızdırdı, esnek katman 5. Katı katmanda kapsama kod yazılmadan sabittir; esnek katmanda her yeni sorgu bir karara bağlıdır.

Sonraki Adım

Parametreli arayüz değeri metnin dışında tutar; tanımlayıcı yerinde ise izin listesi devreye girer. İkisi de metni sabit kabul eder: sorgunun iskeleti geliştiricinin yazdığı şeydir. Bu varsayımın tutmadığı bir yer var. Bildirim metinleri, rapor başlıkları ve fatura açıklama satırları şablonlardan üretiliyor ve bu şablonların bir kısmı çalışma zamanında oluşturuluyor — kimi yalnız değer alıyor, kimi gövdesine kullanıcı verisi alıyor. Sonraki ders şablonları sayar ve tek soruyu ölçer: kaç şablon kullanıcı verisini yalnız değer olarak alıyor, kaçı o veriyi sunucuda değerlendirilen bir ifadenin parçası hâline getiriyor.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat