İçeriğe geç
academia.sh

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 desc hatası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 $raf hatası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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat