İçeriğe geç
academia.sh

Ders 07 / 15

Dinamik Uygulama Güvenlik Testi

Çalışan bir servise istek göndererek yapılan sınamanın ölçülmesi: bağlantı keşfiyle ve rota tablosuyla ulaşılan uç payı, sondaların ürettiği puanlı bulgular üzerinde eşik taraması ve statik taramayla dinamik testin gördüğü kusur sınıflarının kesişimsiz ayrışması.

İçindekiler

Statik tarama kodun ne yazdığını okudu, ne yaptığını değil. Bir satırda duran çağrı hiç ulaşılmayan bir dalda olabilir; koda gömülü bir değer üretimde bir ortam değişkeniyle bastırılıyor olabilir. Tersi de doğrudur: iki dosyanın ayrı ayrı temiz olduğu bir sistemde, uçların birleşimi bir isteği hiç beklenmedik bir yanıtla karşılayabilir.

Dinamik uygulama güvenlik testi bu yüzden kaynağa değil koşan sürece bakar: uçlara istek gönderir, yanıtları okur ve yanıtın kendisini kanıt sayar. Bu dersin ölçtüğü üç şey var: bu testin uçların ne kadarına ulaşabildiği, kanıtı bulguya çeviren eşiğin nerede durduğu ve iki yöntemin gördüğü kusur sınıflarının nerede ayrıştığı.

Sınanacak Servis

Ödünç servisinin sekiz ucu var. Beş kusuru elle doğrulanmıştır; sonuçlar bu listeyle karşılaştırılacak.

// servis.mjs — odunc servisinin sekiz ucu; baslat() dinleyen bir sunucu dondurur
import { createServer } from "node:http";

const KITAP = { "978-1": "Kum Saati", "978-2": "Rafta Kalan" };
const UYE = { 1: { ad: "A. Yilmaz", ceza: 0 }, 2: { ad: "B. Demir", ceza: 12 } };

export const UCLAR = ["/", "/katalog", "/kitap", "/durum", "/uye", "/odunc", "/rapor", "/yonetim/gunluk"];

// Elle dogrulanmis kusurlar (uc | sinif).
export const GERCEK = [
  "/kitap|girdi-yankisi", "/kitap|bos-girdi-ayrintisi", "/uye|kimliksiz-erisim",
  "/odunc|kimliksiz-kabul", "/yonetim/gunluk|ic-ayrinti",
];

export function baslat() {
  const s = createServer((istek, yanit) => {
    const u = new URL(istek.url, "http://y");
    const gonder = (kod, govde) => yanit.writeHead(kod, { "content-type": "application/json" })
      .end(JSON.stringify(govde));

    if (u.pathname === "/") return gonder(200, { baglanti: ["/katalog", "/durum"] });
    if (u.pathname === "/katalog")
      return gonder(200, { kitap: Object.keys(KITAP).map((i) => `/kitap?isbn=${i}`) });
    if (u.pathname === "/kitap") {
      const isbn = u.searchParams.get("isbn") ?? "";
      if (isbn === "")                                        // yalniz bos girdide acilan dal
        return gonder(400, { hata: "isbn bos", sorgu: "SELECT baslik FROM kitap WHERE isbn = ''" });
      return KITAP[isbn] ? gonder(200, { isbn, baslik: KITAP[isbn] })
        : gonder(404, { hata: `kayit yok: ${isbn}` });          // girdi oldugu gibi yankilanir
    }
    if (u.pathname === "/durum") return gonder(200, { durum: "acik", kuyruk: 0 });
    if (u.pathname.startsWith("/uye/")) {
      const no = u.pathname.slice(5);
      return UYE[no] ? gonder(200, { no, ...UYE[no] }) : gonder(404, { hata: "uye yok" });
    }
    if (u.pathname === "/odunc" && istek.method === "POST") return gonder(202, { alindi: true });
    if (u.pathname === "/rapor") return gonder(200, { satir: Object.keys(UYE).length });
    if (u.pathname === "/yonetim/gunluk")
      return gonder(500, { hata: "gunluk okunamadi", yol: "/srv/odunc/var/log/odunc.log" });
    return gonder(404, { hata: "yol yok" });
  });
  return s;
}

export const dinle = (s) => new Promise((c) => s.listen(0, "127.0.0.1", () => c(s.address().port)));

Kapsanan Uç Payı

Dinamik testin ilk sınırı ulaşabildiği yüzeydir. Yalnız kök uçtan başlayıp yanıtlarda geçen bağlantıları izleyen bir keşif ile, kaynaktan çıkarılmış rota tablosu verilen bir koşum aynı servis üzerinde ayrı sonuçlar verir.

// kesif.mjs — dinamik testin ulasabildigi uc payi: yalniz baglantidan ve rota tablosuyla
import { baslat, dinle, UCLAR, GERCEK } from "./servis.mjs";

const s = baslat();
const p = await dinle(s);
const kok = `http://127.0.0.1:${p}`;
const yol = (u) => (u.split("?")[0].startsWith("/uye/") ? "/uye" : u.split("?")[0]);

// (a) Yalniz "/" ucundan baslayip yanitlarda gecen baglantilari izleyen kesif.
const gorulen = new Set(), sira = ["/"];
let istek = 0;
while (sira.length) {
  const u = sira.shift();
  if (gorulen.has(yol(u))) continue;
  gorulen.add(yol(u));
  const govde = await (await fetch(kok + u)).text();
  istek += 1;
  for (const m of govde.matchAll(/"(\/[^"]*)"/g)) if (!gorulen.has(yol(m[1]))) sira.push(m[1]);
}
console.log(`baglantidan kesif : ${istek} istek, ${gorulen.size}/${UCLAR.length} uc` +
  ` = %${((100 * gorulen.size) / UCLAR.length).toFixed(0)}`);
const disarida = UCLAR.filter((u) => !gorulen.has(u));
console.log(`  ulasilamayan    : ${disarida.join(", ")}`);
console.log(`  bu uclarda duran kusur: ${GERCEK.filter((g) => disarida.includes(g.split("|")[0])).length}/${GERCEK.length}`);

// (b) Kaynaktan cikarilan rota tablosu verilirse ayni kosum butun uclara dokunur.
const rota = new Set();
for (const u of UCLAR) {
  const hedef = u === "/uye" ? "/uye/1" : u;
  const y = await fetch(kok + hedef, u === "/odunc" ? { method: "POST", body: "{}" } : undefined);
  if (y.status !== 404 || u === "/kitap") rota.add(u);
  await y.text();
}
console.log(`rota tablosuyla   : ${UCLAR.length} istek, ${rota.size}/${UCLAR.length} uc` +
  ` = %${((100 * rota.size) / UCLAR.length).toFixed(0)}`);
s.close();
baglantidan kesif : 4 istek, 4/8 uc = %50
  ulasilamayan    : /uye, /odunc, /rapor, /yonetim/gunluk
  bu uclarda duran kusur: 3/5
rota tablosuyla   : 8 istek, 8/8 uc = %100

Bağlantı izleyerek çalışan keşif uçların yarısını gördü ve gördüğü yarıda kusurların yalnız ikisi vardı. Ulaşılamayan dördü hiçbir yanıtta anılmıyor: biri gövde ister, biri yönetim tarafında, ikisi bağlantı listelerinde geçmiyor. Kapsama eşiğinin kaynağı burada bir ölçümdür: kapsanmayan uçlarda duran kusur sayısı 3/5 çıktığı için “uçların tamamına dokunulmadan koşum kabul edilmez” kuralı savunulabilir hâle gelir.

Bu eşiği tutturmanın yolu ilginçtir: rota tablosu kaynaktan çıkarılır. Dinamik testin kapsamasını tavana çıkaran şey statik bir üründür; iki yöntem burada birbirini besler.

Sondalar ve Eşik Taraması

Her uca tek bir istek gider. Yanıt üç kurala göre puanlanır: gönderilen zararsız işaretçi gövdede aynen görünüyor mu, kimlik bildirilmeden kişisel alan dönüyor mu, gövdede iç ayrıntı geçiyor mu. Puanların anlamı bir önceki dersteki gibidir; ağırlık IO8’den gelir ve w = 5 olarak sürer.

// sonda.mjs — her uca tek istek gonderen sondalar, puanlanmis bulgular ve esik taramasi
import { baslat, dinle, UCLAR, GERCEK } from "./servis.mjs";

const IZ = "ISBN-KONTROL-7391";                 // zararsiz isaretci: yanitta aranacak
const KISISEL = ["ad", "ceza"], SAYAC = ["kuyruk", "satir"];

const s = baslat();
const kok = `http://127.0.0.1:${await dinle(s)}`;
const bulgu = [];
for (const uc of UCLAR) {
  const post = uc === "/odunc";
  const hedef = uc === "/uye" ? "/uye/2" : `${uc}?isbn=${IZ}`;
  const y = await fetch(kok + hedef, post ? { method: "POST", body: "{}" } : undefined);
  const g = await y.text();
  if (g.includes(IZ)) bulgu.push({ uc, sinif: "girdi-yankisi", puan: 80 });
  if (y.status < 300 && KISISEL.some((a) => g.includes(`"${a}"`))) bulgu.push({ uc, sinif: "kimliksiz-erisim", puan: 90 });
  if (y.status < 300 && post) bulgu.push({ uc, sinif: "kimliksiz-kabul", puan: 50 });
  if (/"\/(srv|var|home)\//.test(g)) bulgu.push({ uc, sinif: "ic-ayrinti", puan: 85 });
  else if (SAYAC.some((a) => g.includes(`"${a}"`))) bulgu.push({ uc, sinif: "ic-sayac", puan: 40 });
}
s.close();

const anahtar = (b) => `${b.uc}|${b.sinif}`;
console.log(`${UCLAR.length} uc, ${UCLAR.length} istek, ${bulgu.length} bulgu, ${GERCEK.length} elle dogrulanmis kusur\n`);
for (const b of bulgu.sort((a, c) => c.puan - a.puan)) {
  console.log(`  ${b.uc.padEnd(17)}${b.sinif.padEnd(20)}${String(b.puan).padStart(3)}  gercek: ${GERCEK.includes(anahtar(b)) ? "evet" : "hayir"}`);
}

console.log(`\n${"esik".padStart(4)}${"kalan".padStart(7)}${"yanlis alarm".padStart(14)}${"kacirma".padStart(9)}${"maliyet w=5".padStart(12)}`);
for (const e of [30, 40, 50, 60, 80, 90]) {
  const k = bulgu.filter((b) => b.puan >= e);
  const ya = k.filter((b) => !GERCEK.includes(anahtar(b))).length;
  const kacirma = GERCEK.length - k.filter((b) => GERCEK.includes(anahtar(b))).length;
  console.log(`${String(e).padStart(4)}${String(k.length).padStart(7)}${String(ya).padStart(14)}${String(kacirma).padStart(9)}${String(ya + 5 * kacirma).padStart(12)}`);
}
console.log(`hicbir esikte gorunmeyen: ${GERCEK.filter((g) => !bulgu.some((b) => anahtar(b) === g)).join(", ")}`);
8 uc, 8 istek, 6 bulgu, 5 elle dogrulanmis kusur

  /uye             kimliksiz-erisim     90  gercek: evet
  /yonetim/gunluk  ic-ayrinti           85  gercek: evet
  /kitap           girdi-yankisi        80  gercek: evet
  /odunc           kimliksiz-kabul      50  gercek: evet
  /durum           ic-sayac             40  gercek: hayir
  /rapor           ic-sayac             40  gercek: hayir

esik  kalan  yanlis alarm  kacirma maliyet w=5
  30      6             2        1           7
  40      6             2        1           7
  50      4             0        1           5
  60      3             0        2          10
  80      3             0        2          10
  90      1             0        4          20
hicbir esikte gorunmeyen: /kitap|bos-girdi-ayrintisi

En düşük maliyet eşik 50’de: iki yanlış alarm düşüyor, dört gerçek kusur kalıyor. Eşiğin bir önceki dersten farkı, bulguların puan ekseninde ayrık kümelenmesidir. Statik taramada gerçek ve masum bulgular aynı bantta duruyordu; burada iki yanlış alarm 40’ta, dört gerçek kusur 50 ve üstünde. Nedeni yöntemin doğasıdır: kanıt yanıtın kendisidir, niyet tahmini değil.

Yakalanan sınıflar kimliksiz erişim, kimliksiz kabul, girdi yankısı ve iç ayrıntı sızıntısı. Dördü de yalnız koşarken görünür. Kaçan sınıf son satırdadır: yalnız boş girdide açılan dal hiçbir eşikte görünmedi, çünkü sonda o girdiyi hiç göndermedi. Dinamik test, gönderdiği girdinin açtığı yolları görür; göndermediği girdi kod tabanında sessiz durur.

İki Yöntemin Ayrıştığı Yer

Aynı servise bir önceki dersin üç statik kuralı uygulanırsa ne çıkar?

// ayrisma.mjs — ayni servise statik kurallarin uygulanmasi ve iki yontemin gordugu sinif kumesi
import { readFileSync } from "node:fs";

const gizli = /\b(\w*(?:anahtar|parola|jeton|gizli)\w*)\s*=\s*["'][^"']{8,}["']/i;
const KURAL = [
  ["yasak-cagri", /\beval\s*\(|\bnew\s+Function\s*\(/],
  ["birlestirilmis-sorgu", /(SELECT|INSERT|UPDATE|DELETE)[^"'`]*["'`]\s*\+\s*\w+/i],
  ["sabit-gizli-deger", gizli],
];

const satir = readFileSync(new URL("./servis.mjs", import.meta.url), "utf8").split("\n");
const statikBulgu = satir.flatMap((s, i) =>
  KURAL.filter(([, d]) => d.test(s)).map(([ad]) => `servis.mjs:${i + 1} ${ad}`));
console.log(`servis.mjs: ${satir.length} satir, ${KURAL.length} kural -> ${statikBulgu.length} statik bulgu`);

// IO9: her kusur sinifinin iki yontemce gorulebilirligi elle siniflandirildi.
const SINIF = [                                  // [sinif, statik gorur, dinamik gorur]
  ["birlestirilmis-sorgu", true, false],
  ["sabit-gizli-deger", true, false],
  ["yasak-cagri", true, false],
  ["girdi-yankisi", false, true],
  ["kimliksiz-erisim", false, true],
  ["kimliksiz-kabul", false, true],
  ["ic-ayrinti", false, true],
  ["sablon-dizgisiyle-sorgu", false, false],
  ["bos-girdi-ayrintisi", false, false],
];

const say = (s, d) => SINIF.filter(([, a, b]) => a === s && b === d).length;
console.log(`\n${SINIF.length} kusur sinifi:`);
console.log(`  yalniz statik : ${say(true, false)}  (${SINIF.filter(([, a, b]) => a && !b).map(([n]) => n).join(", ")})`);
console.log(`  yalniz dinamik: ${say(false, true)}  (${SINIF.filter(([, a, b]) => !a && b).map(([n]) => n).join(", ")})`);
console.log(`  ikisi birden  : ${say(true, true)}`);
console.log(`  hicbiri       : ${say(false, false)}  (${SINIF.filter(([, a, b]) => !a && !b).map(([n]) => n).join(", ")})`);
const birlesim = SINIF.filter(([, a, b]) => a || b).length;
console.log(`birlesik kapsama: ${birlesim}/${SINIF.length} = %${((100 * birlesim) / SINIF.length).toFixed(0)}`);
servis.mjs: 46 satir, 3 kural -> 0 statik bulgu

9 kusur sinifi:
  yalniz statik : 3  (birlestirilmis-sorgu, sabit-gizli-deger, yasak-cagri)
  yalniz dinamik: 4  (girdi-yankisi, kimliksiz-erisim, kimliksiz-kabul, ic-ayrinti)
  ikisi birden  : 0
  hicbiri       : 2  (sablon-dizgisiyle-sorgu, bos-girdi-ayrintisi)
birlesik kapsama: 7/9 = %78

Statik kurallar bu dosyada tek bulgu üretmedi; dinamik sondalar aynı dosyada dört gerçek kusur buldu. Ters yön de aynı derecede keskin: bir önceki dersin kod tabanındaki gömülü değer ve birleştirilmiş sorgu, buradaki hiçbir istekle görünmez. Bu kurulumda iki yöntemin kesişimi boştur ve birleşik kapsama dokuz sınıfın yedisidir. Kural kümeleri büyüdükçe kesişim de büyür, ama biri diğerinin yerine geçmez.

Kararın sahibi: eşik 50’nin üstünde kalan bulgu yayımı durdurur; kapsama eşiğini tutturamayan koşum ise sonuç bildirmez, yeniden koşar. Kapsamasını bildirmeyen bir dinamik test raporu, kaç ucun hiç denenmediğini gizler.

Testin Bedeli

Koşumdan bağımsız bedel üç yerdedir. Birincisi istek sayısı: 8 uç, koşum başına 8 istek ve ayakta duran bir süreç. Statik tarama sıfır süreçle çalışıyordu; dinamik test uygulamanın kendisini, verisini ve bağımlılıklarını ister.

İkincisi rota tablosunun bakımı: kapsamayı %50’den %100’e çıkaran şey kaynaktan çıkarılan sekiz satırlık listedir ve yeni bir uç eklendiğinde bu liste elle güncellenmezse kapsama sessizce düşer.

Üçüncüsü girdi kümesi: sondalar yalnız gönderdikleri değerin açtığı dalları görür. Boş girdi dalı bir tek değer eksik olduğu için kaçtı. Girdi kümesini büyütmek istek sayısını uç sayısıyla çarpar; kapsama doğrusal artarken maliyet çarpımsal artar.

Özet

  • Dinamik uygulama güvenlik testi koşan sürece istek gönderir ve kanıtı yanıtın kendisinden alır; bu derste 8 uca 8 istekle 6 bulgu üretildi, 4’ü gerçek kusurdu.
  • Bağlantı izleyen keşif uçların %50’sine ulaştı ve elle doğrulanmış 5 kusurun 3’ü ulaşılamayan uçlarda kaldı; rota tablosu verildiğinde kapsama %100’e çıktı.
  • Eşik taraması en düşük maliyeti 50’de verdi: 0 yanlış kalma, 1 yanlış geçme. Bulgular puan ekseninde ayrık kümelendiği için eşik seçimi statik taramadakinden daha kolaydır.
  • Yakalanan sınıflar yalnız koşarken görünür olanlardır; kaçan sınıf, sondanın hiç göndermediği girdinin açtığı daldır.
  • Aynı servise uygulanan üç statik kural sıfır bulgu verdi; dokuz kusur sınıfında iki yöntemin kesişimi boş çıktı ve birleşik kapsama %78’de kaldı.
  • Bedel: ayakta bir süreç, koşum başına 8 istek, elle bakılan bir rota tablosu ve girdi kümesi büyüdükçe çarpımsal büyüyen istek sayısı.

Sonraki Adım

İki tarama da yazılan kodu sınadı: biri metnini okudu, öteki koştururken davranışını gözledi. Oysa ödünç servisinin çalıştırdığı satırların çoğunu bu takım yazmadı. Katalog şeması bir doğrulayıcıya, bildirim kanalı bir kuyruk istemcisine, günlük yazımı bir biçimleyiciye dayanıyor ve bunların her biri kendi bağımlılıklarını getiriyor. Bu kodda kusur aramanın yolu taramak değil, bilinen kusurları aramaktır: bir bileşenin hangi sürümünde hangi kusurun bildirildiği kayıtlıdır. Sonraki ders ödünç sisteminin bağımlılık ağacını çıkarır, kendi kayıt kümesini bu ağaçla eşleştirir ve üç şeyi ölçer: doğrudan yazılmamış bağımlılıkların payı, sürüm aralığı eşleşmesinin ürettiği yanlış alarm ve tek bir düzeltmenin kaç pakete dokunduğu.

İ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