Ders 02 / 16
Enjeksiyon Sınıfı Açıklar
Sorgu, komut ve şablon enjeksiyonunun tek bir kökten ölçülmesi: on meşru arama girdisinin birleştirilmiş sorguda kaç beklenmeyen sonuç ürettiği, altı rapor etiketinin tek dizge olarak geçirildiğinde kaçının bozulduğu, beş not metninin şablon gövdesine yazıldığında kaçının değiştiği ve on iki çağrı yerinin kaçının birleştirme kullandığı.
İçindekiler
Önceki ders sınırın biçimini kurdu: gövdenin alanları, tipleri ve aralıkları sözleşmeye uyuyor mu. Sözleşmeyi geçen değer artık doğru biçimdedir, ama hâlâ bir metindir ve sistemin içinde bir yere yazılacaktır. Bu ders o yazma anını ölçer.
Üç ayrı sınıf sayılır — sorgu, komut ve şablon — ve üçü de aynı karara iner: değer, o bağlamın metnine birleştirildi mi, yoksa ayrı bir işlenen olarak mı geçirildi? Ölçüm boyunca hiçbir kötü niyetli girdi kullanılmaz. Girdilerin hepsi kütüphane kataloğunda gerçekten geçen metinlerdir: kesme işaretli yazar adları, süslü parantez içeren görevli notları, boşluklu rapor etiketleri. Ölçülen şey kaç meşru girdinin beklenmeyen sonuç ürettiğidir.
BG4 (varsayım): iki kurulusun aynı davranması beklenir. Ayrı işlenen yolu ölçüt kabul edilir; birleşik yolun ondan farklı her sonucu — hata da, farklı satır da — beklenmeyen sonuç sayılır.
Sorgu Bağlamı
Katalog arama ucu, ödünç sisteminin en çok çağrılan yeridir. Aynı arama iki kurulusla yazılır ve on meşru girdiye ayrı ayrı sorulur.
// veri.mjs — odunc katalogu ve gercek kataloglarda gecen mesru arama girdileri import { DatabaseSync } from "node:sqlite"; export const db = new DatabaseSync(":memory:"); db.exec(`CREATE TABLE kitap (isbn TEXT PRIMARY KEY, baslik TEXT, yazar TEXT, yil INTEGER)`); const ekle = db.prepare(`INSERT INTO kitap VALUES (?, ?, ?, ?)`); for (const s of [ ["9789750718533", "Kayip Zaman", "M. Proust", 1913], ["9786050000000", "Rock 'n' Roll Tarihi", "J. O'Neill", 1998], ["9789750000001", "100% Pamuk", "A. Saenz", 2004], ["9789750000002", "L'Etranger", "A. Camus", 1942], ["9789750000003", "Kar_ Deseni", "H. Yilmaz", 2011], ["9789750000004", "Tarih -- 20. Yuzyil", "S. Aydin", 2019], ["9789750000005", 'Deneme "iki"', "N. Kaya", 2020], ["9789750000006", "Kalem ve Kagit", "B. Demir", 1995], ]) ekle.run(...s); // Hicbiri saldiri degil; hepsi kutuphane katalogunda gecen mesru arama metinleri. export const ARAMA = ["Kalem", "O'Neill", "100%", "Kar_", "--", 'iki"', "Rock 'n' Roll", "Camus", "L'Etranger", "Kaya'nin"]; // Siralama alani: sozlesme bir alan adi bekler. export const SIRA = ["baslik", "yil", "yazar", "yil desc", "kitap.ic_not"];
// sorgu.mjs — ayni arama, iki kurulus: ayri islenen ve dizge birlestirme import { db, ARAMA, SIRA } from "./veri.mjs"; const dene = (f) => { try { return { ok: 1, v: f() }; } catch (e) { return { ok: 0, v: e.message.slice(0, 34) }; } }; const islenen = (q) => db.prepare(`SELECT isbn FROM kitap WHERE baslik LIKE ? OR yazar LIKE ?`) .all(`%${q}%`, `%${q}%`).map((r) => r.isbn); const birlesik = (q) => db.prepare( `SELECT isbn FROM kitap WHERE baslik LIKE '%${q}%' OR yazar LIKE '%${q}%'`).all().map((r) => r.isbn); console.log(`${ARAMA.length} mesru arama girdisi, iki kurulus\n`); console.log(`${"girdi".padEnd(16)}${"islenen".padStart(9)}${"birlesik".padStart(10)} fark`); let sapan = 0, hatali = 0; for (const q of ARAMA) { const a = dene(() => islenen(q)), b = dene(() => birlesik(q)); const ayni = a.ok && b.ok && String(a.v) === String(b.v); if (!ayni) sapan++; if (!b.ok) hatali++; console.log(JSON.stringify(q).padEnd(16) + (a.ok ? String(a.v.length) : "hata").padStart(9) + (b.ok ? String(b.v.length) : "hata").padStart(10) + " " + (ayni ? "-" : b.ok ? "farkli satir" : b.v)); } console.log(`\nbirlesik kurulusta beklenmeyen sonuc: ${sapan}/${ARAMA.length} (${hatali} tanesi hata)`); // Alan adi islenen olarak gecirilemez; burada birlestirme kacinilmazdir. const IZIN = { baslik: "baslik", yil: "yil", yazar: "yazar" }; const sirala = (alan, izinli) => { if (izinli && !IZIN[alan]) return "ret"; const s = izinli ? IZIN[alan] : alan; return dene(() => db.prepare(`SELECT isbn FROM kitap ORDER BY ${s}`).all()[0].isbn); }; console.log(`\n${"siralama girdisi".padEnd(18)}${"birlestirme".padStart(18)}${"izin listesi".padStart(18)}`); for (const alan of SIRA) { const b = sirala(alan, false), i = sirala(alan, true); const yaz = (r) => (r === "ret" ? "ret" : r.ok ? r.v : "hata"); console.log(alan.padEnd(18) + yaz(b).padStart(18) + yaz(i).padStart(18)); }
10 mesru arama girdisi, iki kurulus girdi islenen birlesik fark "Kalem" 1 1 - "O'Neill" 1 hata near "Neill": syntax error "100%" 1 1 - "Kar_" 1 1 - "--" 1 1 - "iki\"" 1 1 - "Rock 'n' Roll" 1 hata near "n": syntax error "Camus" 1 1 - "L'Etranger" 1 hata near "Etranger": syntax error "Kaya'nin" 0 hata near "nin": syntax error birlesik kurulusta beklenmeyen sonuc: 4/10 (4 tanesi hata) siralama girdisi birlestirme izin listesi baslik 9789750000001 9789750000001 yil 9789750718533 9789750718533 yazar 9789750000002 9789750000002 yil desc 9789750000005 ret kitap.ic_not hata ret
Dört girdi sapıyor ve dördünün de ortak özelliği bir kesme işareti taşımasıdır. Bu dört okuyucunun araması çalışmıyor — ödünç sistemi bir yazar adını arayamıyor. Enjeksiyon sınıfının en sık görülen üretim etkisi budur: açık değil, kırık işlev.
İkinci tablo asıl noktadır. Sıralama alanı bir işlenen olarak geçirilemez, çünkü sorgu
metninin kendisinin bir parçasıdır; orada birleştirme kaçınılmazdır. yil desc girdisi
birleştirmede hata vermeden kabul ediliyor ve farklı bir satır dönüyor. kitap.ic_not
hata veriyor. İzin listesi ikisini de reddediyor. Birleştirmenin zorunlu olduğu tek yer,
sapmanın sessiz olduğu tek yerdir — ve orada tek çare önceki dersin izin listesidir.
Komut Bağlamı
Rapor servisi, dışa aktarma işini ayrı bir sürece veriyor. Etiket iki yolla geçirilebilir: ayrı bir argüman olarak ya da komut satırına birleştirilmiş bir dizge içinde.
// yankila.mjs — rapor sureci: aldigi argumanlari oldugu gibi basar console.log(JSON.stringify(process.argv.slice(2)));
// komut.mjs — rapor etiketi alt surece iki yolla gecirilir: ayri arguman ve tek dizge import { execFileSync, execSync } from "node:child_process"; // Hepsi kutuphane gorevlisinin yazdigi mesru rapor etiketleri. const ETIKET = ["2026-03", "gunluk rapor", "bolum $raf", "O'Neill listesi", "ikinci el", "kayip-bulunan"]; const oku = (f) => { try { return JSON.parse(f()); } catch { return null; } }; const islenen = (e) => oku(() => execFileSync("node", ["yankila.mjs", e], { encoding: "utf8" })); const birlesik = (e) => oku(() => execSync(`node yankila.mjs ${e}`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] })); console.log(`${ETIKET.length} mesru rapor etiketi, iki gecirme yolu\n`); console.log(`${"etiket".padEnd(19)}${"ayri arguman".padStart(22)}${"tek dizge".padStart(26)}`); let bozuk = 0; for (const e of ETIKET) { const a = islenen(e), b = birlesik(e); const ayni = b && b.length === 1 && b[0] === e; if (!ayni) bozuk++; console.log(e.padEnd(19) + JSON.stringify(a).padStart(22) + (b ? JSON.stringify(b) : "surec hata").padStart(26)); } console.log(`\nayri arguman: 0/${ETIKET.length} bozuldu, tek dizge: ${bozuk}/${ETIKET.length} bozuldu`);
6 mesru rapor etiketi, iki gecirme yolu etiket ayri arguman tek dizge 2026-03 ["2026-03"] ["2026-03"] gunluk rapor ["gunluk rapor"] ["gunluk","rapor"] bolum $raf ["bolum $raf"] ["bolum"] O'Neill listesi ["O'Neill listesi"] surec hata ikinci el ["ikinci el"] ["ikinci","el"] kayip-bulunan ["kayip-bulunan"] ["kayip-bulunan"] ayri arguman: 0/6 bozuldu, tek dizge: 4/6 bozuldu
Aynı kök, farklı dil. Boşluklu etiket iki argümana bölünüyor, çift boşluk teke iniyor,
kesme işaretli etiket süreci hiç başlatmıyor. Üçüncü satır bu bölümün sessiz vakasıdır:
bolum $raf etiketi hatasız çalışıyor ve alt sürece bolum olarak varıyor. Rapor
üretilir, dosya yazılır, kimse bir şey fark etmez; yalnız etiketin yarısı yoktur.
Şablon Bağlamı
Bildirim metni bir şablondan üretiliyor. Görevlinin yazdığı not ya şablona veri olarak verilir ya da şablonun gövdesine yazılıp sonra işlenir.
// sablon.mjs — bildirim metni iki yolla kurulur: veri islenen olarak, veri sablonun icine const SABLON = "Sayin {ad}, {isbn} son gunu {gun}. Not: {not}"; const YERLESTIR = (metin, veri) => metin.replace(/\{(\w+)\}/g, (_, k) => veri[k] ?? ""); // Hepsi gorevlinin elle yazdigi mesru not metinleri. const NOTLAR = ["kapak yipranmis", "iade tarihi {gun} olarak yazilmis", "raf {yer} bulunamadi", "ucret 5 TL", "{ad} adina kayitli"]; const TEMEL = { ad: "A. Kaya", isbn: "9789750718533", gun: "2026-03-25" }; // Ayri islenen: sablon sabittir, veri bir kez yerine konur ve bir daha taranmaz. const islenen = (not) => YERLESTIR(SABLON, { ...TEMEL, not }); // Birlestirme: veri once sablonun govdesine yazilir, sonra govde islenir. const birlesik = (not) => YERLESTIR(SABLON.replace("{not}", not), { ...TEMEL, not }); const notAlani = (m) => m.split("Not: ")[1]; console.log(`${NOTLAR.length} mesru not metni, iki kurulus\n`); console.log(`${"girilen not".padEnd(36)}${"birlesik kurulusun bastigi not".padEnd(40)}fark`); let bozuk = 0; for (const not of NOTLAR) { const b = birlesik(not), ayni = b === islenen(not); if (!ayni) bozuk++; console.log(not.padEnd(36) + notAlani(b).padEnd(40) + (ayni ? "-" : "degisti")); } console.log(`\nnot alani girildigi gibi cikmayan bildirim: birlesik ${bozuk}/${NOTLAR.length}, islenen 0/${NOTLAR.length}`);
5 mesru not metni, iki kurulus
girilen not birlesik kurulusun bastigi not fark
kapak yipranmis kapak yipranmis -
iade tarihi {gun} olarak yazilmis iade tarihi 2026-03-25 olarak yazilmis degisti
raf {yer} bulunamadi raf bulunamadi degisti
ucret 5 TL ucret 5 TL -
{ad} adina kayitli A. Kaya adina kayitli degisti
not alani girildigi gibi cikmayan bildirim: birlesik 3/5, islenen 0/5
Beş nottan üçü, okuyucuya giden bildirimde başka bir metne dönüştü. Hiçbiri hata vermedi. Bir tanesi bilinmeyen bir anahtar taşıdığı için boşaldı; kayıt duruyor, bildirimde yok. Ayrı işlenen yolunda şablon sabittir ve veri bir kez yerine konup bir daha taranmaz — bu yüzden beşi de girildiği gibi çıkıyor.
Birleştirmenin Sayımı
Karar tek bir yerde verilmez; her çağrı yerinde ayrı ayrı verilir. Bölünmüş sistemin çağrı yerleri sayılıyor.
// cagri.mjs — bolunmus sistemdeki cagri yerlerinin sayimi ve tek unutulmus yerin olcusu import { db, ARAMA } from "./veri.mjs"; // [servis, cagri yeri, baglam, kurulus] const CAGRI = [ ["katalog", "ara", "sorgu", "islenen"], ["katalog", "sirala", "sorgu", "birlestirme + izin listesi"], ["katalog", "isbnGetir", "sorgu", "islenen"], ["odunc", "ver", "sorgu", "islenen"], ["odunc", "gecikmis", "sorgu", "islenen"], ["odunc", "arsivAra", "sorgu", "birlestirme"], // gozden kacan tek yer ["uye", "notEkle", "sorgu", "islenen"], ["uye", "gecmis", "sorgu", "islenen"], ["bildirim", "bicimle", "sablon", "islenen"], ["bildirim", "topluGonder", "sablon", "islenen"], ["rapor", "disaAktar", "komut", "islenen"], ["rapor", "arsivle", "komut", "islenen"], ]; const say = (k) => CAGRI.filter(k).length; console.log(`${CAGRI.length} cagri yeri, ${new Set(CAGRI.map((c) => c[0])).size} servis`); for (const b of ["sorgu", "komut", "sablon"]) console.log(` ${b.padEnd(7)} ${say((c) => c[2] === b)} yer, ` + `${say((c) => c[2] === b && c[3].startsWith("birlestirme"))} tanesi birlestirme`); console.log(` korumasiz birlestirme: ${say((c) => c[3] === "birlestirme")} ` + `(${CAGRI.filter((c) => c[3] === "birlestirme").map((c) => c[0] + "." + c[1]).join(", ")})`); // Gozden kacan yer gercekten kosuluyor: ayni arama girdileri, tek fark kurulus. const arsivAra = (q) => db.prepare(`SELECT isbn FROM kitap WHERE baslik LIKE '%${q}%'`).all(); let sapan = 0; const sapanlar = []; for (const q of ARAMA) { try { arsivAra(q); } catch { sapan++; sapanlar.push(q); } } console.log(`\ntek unutulmus yerde ${ARAMA.length} girdiden ${sapan} tanesi beklenmeyen sonuc verdi`); console.log(`sapanlarin ortak ozelligi: hepsinde kesme isareti var -> ${sapanlar.every((q) => q.includes("'"))}`); console.log(`kesme isareti tasimayan ${ARAMA.filter((q) => !q.includes("'")).length} girdi ile kurulan bir sinama kumesi bu yeri hic gormez`);
12 cagri yeri, 5 servis sorgu 8 yer, 2 tanesi birlestirme komut 2 yer, 0 tanesi birlestirme sablon 2 yer, 0 tanesi birlestirme korumasiz birlestirme: 1 (odunc.arsivAra) tek unutulmus yerde 10 girdiden 4 tanesi beklenmeyen sonuc verdi sapanlarin ortak ozelligi: hepsinde kesme isareti var -> true kesme isareti tasimayan 6 girdi ile kurulan bir sinama kumesi bu yeri hic gormez
On iki çağrı yerinden ikisi birleştirme kullanıyor; biri sıralama alanıdır ve izin listesiyle korunmuştur, biri gözden kaçmıştır. Sistem çalışıyor, bütün uçlar yanıt veriyor, hiçbir sınama kırmızı değil. Son satır nedenini söylüyor: sapma yalnız kesme işareti taşıyan girdilerde ortaya çıkıyor ve sade adlarla kurulmuş bir sınama kümesi o yeri hiç görmez.
Ayarın yeri ve tekrarı: bu karar bir yapılandırma dosyasında durmaz; on iki çağrı yerinin her birinde ayrı ayrı verilir. Tek yerden değiştirilebilir hâle gelmesi için sorgu, komut ve şablon çağrılarının tek bir sarmalayıcıdan geçmesi gerekir — o zaman karar 12 noktadan 3 noktaya iner.
Özet
- Üç sınıf tek bir karara iniyor: değer bağlamın metnine mi birleştirildi, yoksa ayrı bir işlenen olarak mı geçirildi. Ölçüm boyunca hiçbir kötü niyetli girdi kullanılmadı.
- On meşru arama girdisinin 4’ü birleştirilmiş sorguda beklenmeyen sonuç verdi; dördü de kesme işareti taşıyordu ve dördünde de sonuç bir hataydı — açık değil, kırık arama.
- Sıralama alanı işlenen olarak geçirilemez; birleştirmenin zorunlu olduğu bu tek yerde
sapma sessizdir:
yil deschatasız kabul edildi ve farklı bir satır döndü. İzin listesi reddetti. - Altı rapor etiketinin 4’ü tek dizge olarak geçirildiğinde bozuldu;
bolum $rafhatasız çalışıp alt sürece yarım vardı. Beş nottan 3’ü şablon gövdesine yazıldığında değişti, biri boşaldı. Ayrı işlenen yolunda üç bağlamda da bozulma sıfır. - On iki çağrı yerinden 2’si birleştirme kullanıyor; biri izin listesiyle korunmuş, biri gözden kaçmış. Kaçan yer 10 girdiden 4’ünde sapıyor, kalan 6 girdiyle kurulmuş bir sınama kümesi onu hiç görmüyor. Karar 12 noktada tekrarlanıyor.
Sonraki Adım
Buraya kadarki iki ders isteğin gövdesine baktı: alanlar doğru biçimde mi, değerler metne mi birleştiriliyor. İkisi de isteğin nereden geldiğini sormadı. Oysa ödünç sisteminin uçlarına yalnız kendi arayüzü değil, tarayıcıda açık başka sayfalar da istek gönderebilir ve bu isteklerin bir kısmı okuyucunun oturum bilgisini yanında taşır. Sonraki ders sunucunun bu konudaki tek sözünü — hangi kökenden gelen isteğe ne kadar izin verdiğini — ölçer: ayarın hangi katmanda durduğunu, joker bir değerin kaç kökene kapı açtığını ve kimlik bilgisi taşıyan bir istekte yanlış ayarın neden hata vermeden sessizce başarısız olduğunu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.