İçeriğe geç
academia.sh

Ders 06 / 15

Statik Uygulama Güvenlik Testi

Kaynak kodu koşturmadan tarayan bir denetimin ölçülmesi: üç kurallık bir tarayıcının bilinen kusurlu bir dosya kümesinde ürettiği yanlış alarm ve kaçırma sayıları, eşik taramasının iki maliyet ağırlığı altında farklı eşik seçmesi ve kapı yeşile döndükten sonra kod tabanında duran kusur.

İçindekiler

Buraya kadarki bütün ölçümler sistemin kendi kullanıcısını varsaydı. Yük üreteci geçerli istekler gönderdi, senaryo gerçekçi bir okuyucuyu taklit etti, darboğaz analizi doğru çalışan bir yolun neresinin yavaşladığını sordu. Hiçbir ölçüm, sistemi bilerek kötüye kullanan bir istemcinin ne bulacağını sormadı.

Bu sorunun en ucuz sorulduğu yer kaynak kodudur. Statik uygulama güvenlik testi programı çalıştırmadan metnine bakar: hangi çağrılar yapılıyor, sorgular nasıl kuruluyor, kodun içinde hangi değişmez değerler duruyor. Bu dersin konusu güvenlik açıklarının kendisi değil — onların istemci tarafındaki karşılıkları Frontend Kalitesi kursunda, birleştirilmiş sorgu riski İleri SQL kursunda kuruldu — taramanın kendisinin ölçülmesidir: kaç kusur bulunuyor, kaç uyarı boşa çıkıyor, hangi kusur hiçbir eşikte görünmüyor.

Kod Tabanı ve Bilinen Kusurlar

Ödünç sisteminin altı dosyası taranacak. Kusurların yeri elle doğrulanmıştır; tarayıcının sonucu bu listeyle karşılaştırılacak.

IO7 (varsayım): elle doğrulanmış kusur listesi eksiksizdir. Listede olmayan bir satır sağlam sayılır. Gerçek bir kod tabanında bu varsayım tutmaz; ölçülen oranlar bu listeye göreli oranlardır.

// kaynak.mjs — odunc sisteminin taranacak kod tabani ve elle dogrulanmis kusur listesi
export const KOD = {
  "katalog.mjs": [
    `const ARAMA = "SELECT isbn, baslik FROM kitap WHERE baslik LIKE ?";`,
    `export const ara = (db, q) => db.prepare(ARAMA).all("%" + q + "%");`,
    `export const sirala = (db, alan) =>`,
    `  db.prepare("SELECT isbn FROM kitap ORDER BY " + alan).all();`,
  ],
  "odunc.mjs": [
    `const GUN = 21;`,
    `export function ver(db, uyeNo, isbn) {`,
    `  const s = "INSERT INTO odunc(uyeNo, isbn, gun) VALUES(?, ?, " + GUN + ")";`,
    `  return db.prepare(s).run(uyeNo, isbn);`,
    `}`,
    `export const gecmis = (db, uyeNo) =>`,
    "  db.prepare(`SELECT * FROM odunc WHERE uyeNo = '${uyeNo}'`).all();",
  ],
  "uye.mjs": [
    `const jeton_anahtari = "k9TdQ2vRm7XzP4bH";`,
    `export const imzala = (uyeNo) => uyeNo + ":" + jeton_anahtari.length;`,
    `export const parola_alani = "kullanici-parolasi";`,
  ],
  "bildirim.mjs": [
    `export function bicimle(sablon, veri) {`,
    "  return new Function(\"v\", \"return `\" + sablon + \"`\")(veri);",
    `}`,
    `// eval( ) cagrisi bu dosyadan kaldirildi`,
  ],
  "rapor.mjs": [
    `const kural = { gecikme: "gun > 21", ceza: "gun * 2" };`,
    `export const uygula = (ad, v) => eval(kural[ad]);`,
    `export const basliklar = ["uyeNo", "isbn", "gun"];`,
  ],
  "ayar.mjs": [
    `export const gizli_yol = process.env.ODUNC_ANAHTAR;`,
    `export const kanal_parola = "parola123";`,
    `export const anahtar_adi = "ODUNC_ANAHTAR";`,
    `export const sunucu = "127.0.0.1";`,
  ],
};

// Elle dogrulanmis kusurlar: dosya, satir (1'den), sinif.
export const GERCEK = [
  { dosya: "katalog.mjs", satir: 4, sinif: "birlestirilmis-sorgu" },
  { dosya: "odunc.mjs", satir: 7, sinif: "birlestirilmis-sorgu" },
  { dosya: "uye.mjs", satir: 1, sinif: "sabit-gizli-deger" },
  { dosya: "bildirim.mjs", satir: 2, sinif: "yasak-cagri" },
  { dosya: "rapor.mjs", satir: 2, sinif: "yasak-cagri" },
  { dosya: "ayar.mjs", satir: 2, sinif: "sabit-gizli-deger" },
];

export const satirSayisi = Object.values(KOD).reduce((t, s) => t + s.length, 0);

Tarayıcı üç kuraldan oluşuyor. Her kural bir desen ve bir puan taşır; puan, bulgunun gerçek bir kusur olma olasılığına dair tarayıcının kendi tahminidir.

// tarayici.mjs — uc kurallik statik tarayici; her bulgu bir puan tasir
const gizli = /\b(\w*(?:anahtar|parola|jeton|gizli)\w*)\s*=\s*["']([^"']{8,})["']/i;
const sorgu = /(SELECT|INSERT|UPDATE|DELETE)[^"'`]*["'`]\s*\+\s*(\w+)/i;

export const KURAL = [
  {
    ad: "yasak-cagri", desen: /\beval\s*\(|\bnew\s+Function\s*\(/,
    puan: (s) => (s.trimStart().startsWith("//") ? 20 : 90),
  },
  {
    ad: "birlestirilmis-sorgu", desen: sorgu,
    // Buyuk harfli ad bir sabittir; degisken adi disaridan gelen deger olabilir.
    puan: (s) => (/^[A-Z0-9_]+$/.test(s.match(sorgu)[2]) ? 30 : 80),
  },
  {
    ad: "sabit-gizli-deger", desen: gizli,
    // Uzun ve karisik deger uretilmis bir sir, kisa ya da tek bicimli deger genelde ad.
    puan: (s) => {
      const d = s.match(gizli)[2];
      return d.length >= 16 && /[a-z]/.test(d) && /[A-Z]/.test(d) && /\d/.test(d) ? 85 : 40;
    },
  },
];

export function tara(kod) {
  const bulgu = [];
  for (const [dosya, satirlar] of Object.entries(kod)) {
    satirlar.forEach((s, i) => {
      for (const k of KURAL) if (k.desen.test(s)) bulgu.push({ dosya, satir: i + 1, sinif: k.ad, puan: k.puan(s) });
    });
  }
  return bulgu;
}

export const karsilastirma = (kod) =>
  Object.values(kod).reduce((t, s) => t + s.length, 0) * KURAL.length;

Bulgular

// bulgular.mjs — taramanin bulgulari ve her birinin gercek kusur listesindeki karsiligi
import { KOD, GERCEK, satirSayisi } from "./kaynak.mjs";
import { tara, karsilastirma, KURAL } from "./tarayici.mjs";

const anahtar = (b) => `${b.dosya}:${b.satir}`;
const gercekKume = new Set(GERCEK.map(anahtar));
const bulgu = tara(KOD).sort((a, b) => b.puan - a.puan);

console.log(`${Object.keys(KOD).length} dosya, ${satirSayisi} satir, ${KURAL.length} kural -> ${karsilastirma(KOD)} karsilastirma`);
console.log(`${bulgu.length} bulgu, ${GERCEK.length} elle dogrulanmis kusur\n`);
console.log(`${"yer".padEnd(18)}${"sinif".padEnd(23)}puan  gercek`);
for (const b of bulgu) {
  console.log(`${anahtar(b).padEnd(18)}${b.sinif.padEnd(23)}${String(b.puan).padStart(4)}  ${gercekKume.has(anahtar(b)) ? "evet" : "hayir"}`);
}

const bulunan = new Set(bulgu.map(anahtar));
const hic = GERCEK.filter((g) => !bulunan.has(anahtar(g)));
console.log(`\nhicbir puanda gorunmeyen kusur: ${hic.map((g) => `${anahtar(g)} ${g.sinif}`).join(", ")}`);
6 dosya, 25 satir, 3 kural -> 75 karsilastirma
9 bulgu, 6 elle dogrulanmis kusur

yer               sinif                  puan  gercek
bildirim.mjs:2    yasak-cagri              90  evet
rapor.mjs:2       yasak-cagri              90  evet
uye.mjs:1         sabit-gizli-deger        85  evet
katalog.mjs:4     birlestirilmis-sorgu     80  evet
uye.mjs:3         sabit-gizli-deger        40  hayir
ayar.mjs:2        sabit-gizli-deger        40  evet
ayar.mjs:3        sabit-gizli-deger        40  hayir
odunc.mjs:3       birlestirilmis-sorgu     30  hayir
bildirim.mjs:4    yasak-cagri              20  hayir

hicbir puanda gorunmeyen kusur: odunc.mjs:7 birlestirilmis-sorgu

Dokuz bulgunun beşi gerçek, dördü yanlış alarm. Yanlış alarmların ortak özelliği, desenin tuttuğu ama bağlamın masum olmasıdır: bir yorum satırında geçen çağrı adı, bir sabitle birleştirilen sorgu metni, gizli değer değil alan adı taşıyan iki değişken. Tarayıcı satırı görür, satırın ne anlama geldiğini görmez.

Altıncı kusur hiçbir puanda görünmedi. odunc.mjs içindeki sorgu birleştirme işareti kullanmıyor; değeri şablon dizgisinin içine yerleştiriyor. Desen " + arıyor, orada öyle bir dizi yok. Kaçırılan sınıf budur: aynı riski farklı bir sözdizimiyle yazan kod. Kuralı genişletmek mümkündür, ama her genişletme yeni yanlış alarm getirir; bir sonraki bölüm bu takası sayıyor.

Eşik Taraması

Puanların hepsi kapıya götürülmez. Bir eşik seçilir ve eşiğin altındaki bulgular listelenir ama yayını durdurmaz. Eşik nereden gelir?

IO8 (varsayım): kaçan bir kusurun bedeli, bir yanlış alarmın incelenmesinin beş katıdır. Bu sayı bir ölçümden değil, bir kabulden gelir ve ders boyunca ağırlık w diye anılır. Eşik doğrudan bu ağırlıktan türer.

// esik.mjs — esik taramasi: her esikte yanlis alarm, kacirma ve iki agirlikla maliyet
import { KOD, GERCEK } from "./kaynak.mjs";
import { tara } from "./tarayici.mjs";

const anahtar = (b) => `${b.dosya}:${b.satir}`;
const gercekKume = new Set(GERCEK.map(anahtar));
const bulgu = tara(KOD);

// IO8: kacan bir kusurun bedeli, bir yanlis alarmin incelenmesinin AGIRLIK kati.
const olc = (esik) => {
  const kalan = bulgu.filter((b) => b.puan >= esik);
  const yanlisAlarm = kalan.filter((b) => !gercekKume.has(anahtar(b))).length;
  const gorulen = new Set(kalan.filter((b) => gercekKume.has(anahtar(b))).map(anahtar)).size;
  return { esik, kalan: kalan.length, yanlisAlarm, kacirma: GERCEK.length - gorulen };
};

const ESIKLER = [20, 30, 40, 50, 60, 70, 80, 90];
const satir = ESIKLER.map(olc);
console.log(`${"esik".padStart(4)}${"kalan".padStart(7)}${"yanlis alarm".padStart(14)}${"kacirma".padStart(9)}${"maliyet w=1".padStart(12)}${"maliyet w=5".padStart(12)}`);
for (const s of satir) {
  console.log(`${String(s.esik).padStart(4)}${String(s.kalan).padStart(7)}${String(s.yanlisAlarm).padStart(14)}${String(s.kacirma).padStart(9)}${String(s.yanlisAlarm + s.kacirma).padStart(12)}${String(s.yanlisAlarm + 5 * s.kacirma).padStart(12)}`);
}

for (const w of [1, 5]) {
  const en = satir.reduce((a, b) => (b.yanlisAlarm + w * b.kacirma < a.yanlisAlarm + w * a.kacirma ? b : a));
  console.log(`w=${w} -> en dusuk maliyetli esik ${en.esik} (yanlis alarm ${en.yanlisAlarm}, kacirma ${en.kacirma})`);
}
esik  kalan  yanlis alarm  kacirma maliyet w=1 maliyet w=5
  20      9             4        1           5           9
  30      8             3        1           4           8
  40      7             2        1           3           7
  50      4             0        2           2          10
  60      4             0        2           2          10
  70      4             0        2           2          10
  80      4             0        2           2          10
  90      2             0        4           4          20
w=1 -> en dusuk maliyetli esik 50 (yanlis alarm 0, kacirma 2)
w=5 -> en dusuk maliyetli esik 40 (yanlis alarm 2, kacirma 1)

Tabloda yanlış kalma yanlış alarm sütunudur: sağlam bir satır kapıyı kırmızıya döndürür. Yanlış geçme kaçırma sütunudur: gerçek bir kusur kapıdan geçer. İki sütun ters yönde hareket eder ve aralarında düz bir kazanan yoktur.

Ağırlık kararı değiştiriyor. İki incelemeyi bir kaçırmaya yeğ tutan ekip eşiği 40’ta tutar; incelemenin pahalı olduğu ekip 50’ye çıkar ve iki kusuru bilerek kapıdan geçirir. Aynı tarayıcı, aynı kod, aynı bulgu kümesi — farklı eşik. Eşiğin kaynağı IO8’dir, bir ölçüm değil; ölçülen şey eşiğin sonucudur.

Kapı ve Düzeltme

Eşik 40 seçilir ve tarama, Kalite ve Test Temelleri kursunda tanımlanan kabul kapısı olarak çalışır. Kapı kırmızı döner, dört kusur düzeltilir, kod yeniden taranır.

// kapi.mjs — duzeltme sonrasi yeniden tarama ve esik 40 ile calisan kabul kapisi
import { KOD, GERCEK } from "./kaynak.mjs";
import { tara } from "./tarayici.mjs";

const ESIK = 40;
const anahtar = (b) => `${b.dosya}:${b.satir}`;
const gercekKume = new Set(GERCEK.map(anahtar));

// Dort kusurun duzeltilmis hali; odunc.mjs:7 bilerek dokunulmadan birakildi.
const DUZELTME = {
  "katalog.mjs": { 4: `  db.prepare("SELECT isbn FROM kitap " + SIRA[alan]).all();` },
  "uye.mjs": { 1: `const jeton_anahtari = process.env.ODUNC_ANAHTAR;` },
  "bildirim.mjs": { 2: "  return sablon.replace(/{(\\w+)}/g, (_, a) => veri[a]);" },
  "rapor.mjs": { 2: `export const uygula = (ad, v) => KURALLAR[ad](v);` },
  "ayar.mjs": { 2: `export const kanal_parola = process.env.KANAL_PAROLA;` },
};

const yeni = Object.fromEntries(Object.entries(KOD).map(([d, s]) =>
  [d, s.map((satir, i) => DUZELTME[d]?.[i + 1] ?? satir)]));

const kapi = (kod, bastirilan = []) => {
  const kalan = tara(kod).filter((b) => b.puan >= ESIK && !bastirilan.includes(anahtar(b)));
  return { durum: kalan.length === 0 ? "yesil" : "kirmizi", kalan };
};

for (const [ad, kod] of [["duzeltme oncesi", KOD], ["duzeltme sonrasi", yeni]]) {
  const { durum, kalan } = kapi(kod);
  console.log(`${ad.padEnd(17)} esik ${ESIK} -> ${durum}, ${kalan.length} bulgu` +
    `, ${kalan.filter((b) => !gercekKume.has(anahtar(b))).length} tanesi yanlis alarm`);
  for (const b of kalan) console.log(`  ${anahtar(b).padEnd(16)}${b.sinif.padEnd(23)}${b.puan}`);
}

const BASTIRILAN = ["uye.mjs:3", "ayar.mjs:3"];   // incelendi, alan adi; kayit acildi
console.log(`\nbastirma listesi ${BASTIRILAN.length} satir -> kapi ${kapi(yeni, BASTIRILAN).durum}`);

const kalanGercek = GERCEK.filter((g) =>
  !Object.keys(DUZELTME[g.dosya] ?? {}).includes(String(g.satir)));
console.log(`kapi yesilken kod tabaninda duran gercek kusur: ${kalanGercek.length}` +
  ` (${kalanGercek.map((g) => `${anahtar(g)} ${g.sinif}`).join(", ")})`);
duzeltme oncesi   esik 40 -> kirmizi, 7 bulgu, 2 tanesi yanlis alarm
  katalog.mjs:4   birlestirilmis-sorgu   80
  uye.mjs:1       sabit-gizli-deger      85
  uye.mjs:3       sabit-gizli-deger      40
  bildirim.mjs:2  yasak-cagri            90
  rapor.mjs:2     yasak-cagri            90
  ayar.mjs:2      sabit-gizli-deger      40
  ayar.mjs:3      sabit-gizli-deger      40
duzeltme sonrasi  esik 40 -> kirmizi, 2 bulgu, 2 tanesi yanlis alarm
  uye.mjs:3       sabit-gizli-deger      40
  ayar.mjs:3      sabit-gizli-deger      40

bastirma listesi 2 satir -> kapi yesil
kapi yesilken kod tabaninda duran gercek kusur: 1 (odunc.mjs:7 birlestirilmis-sorgu)

Katalog düzeltmesi sıralama alanını sabit bir tablodan okuyor; kural yine eşleşir ama işleneni büyük harfli bir sabit olduğu için puan 80’den 30’a düşer ve eşiğin altında kalır. Düzeltmeden sonra kapı hâlâ kırmızı ve kalan iki bulgunun ikisi de yanlış alarm. Bu, statik taramanın en çok zaman yiyen yeridir: kapıyı yeşile döndürmek için ya kural daraltılır ya da bulgular tek tek incelenip bastırma listesine yazılır. Liste iki satır uzayınca kapı yeşile döner — ve kod tabanında hâlâ gerçek bir kusur durur.

Kararın sahibi: kırmızı kapı yayımı durdurur, bastırılan her satır bir inceleme kaydı açar. Bastırma listesi zamanla büyür ve gözden geçirilmediğinde taramanın gördüğü alanı sessizce daraltır.

Taramanın Bedeli

Koşumdan bağımsız bedel üç sayıdadır. Birincisi tarama işinin kendisi: 25 satır, 3 kural, 75 karşılaştırma; sıfır süreç ayağa kalkıyor, sıfır istek gidiyor, sıfır test verisi hazırlanıyor. Bu yüzden tarama her değişiklikte koşturulabilir.

İkincisi inceleme: eşik 40’ta 7 bulgu insan gözü ister ve bunların 2’si boşa çıkar. Kod tabanı büyüdükçe bu sayı satır sayısıyla birlikte büyür, kusur sayısıyla değil.

Üçüncüsü bakım: üç kural ve iki satırlık bastırma listesi elle yazılmıştır. Yeni bir sözdizimi kullanıma girdiğinde kural sessiz kalır — odunc.mjs:7 bunun kanıtıdır — ve sessiz kalan bir kural, olmayan bir kuraldan ayırt edilemez.

Özet

  • Statik uygulama güvenlik testi kaynağı çalıştırmadan tarar; bu derste üç kurallık bir tarayıcı 6 dosya ve 25 satır üzerinde 9 bulgu üretti, bunların 5’i gerçek kusurdu.
  • Yanlış alarmların ortak nedeni bağlam körlüğüdür: yorum satırı, sabitle birleştirilen sorgu ve gizli değer değil alan adı taşıyan değişkenler desene takıldı.
  • Eşik taraması iki sütunu birlikte verir: eşik 40’ta 2 yanlış kalma ve 1 yanlış geçme, eşik 50’de 0 yanlış kalma ve 2 yanlış geçme. Seçim, IO8’in ağırlığından türer.
  • Yakalanan sınıf, desenle eşleşen kusurlu satırdır; kaçan sınıf aynı riski farklı sözdizimiyle yazan koddur — şablon dizgisiyle kurulan sorgu hiçbir eşikte görünmedi.
  • Bedel: 75 karşılaştırma, sıfır süreç, sıfır istek; karşılığında eşik 40’ta 7 bulgunun elle incelenmesi ve büyüyen bir bastırma listesinin bakımı.
  • Kapı yeşile döndüğünde kod tabanında hâlâ bir gerçek kusur duruyordu; yeşil kapı kusursuz kod anlamına gelmez, yalnız kuralların sustuğu anlamına gelir.

Sonraki Adım

Bu taramanın gördüğü her şey metinden okundu: hangi çağrı yazılmış, sorgu nasıl kurulmuş, hangi değer koda gömülmüş. Metin ise programın ne yaptığını değil, ne yazdığını söyler. rapor.mjs içindeki çağrı hiç ulaşılmayan bir dalda olabilir; ayar.mjs içindeki değer üretimde ortam değişkeniyle bastırılıyor olabilir. Tersi de doğrudur: iki dosyanın ayrı ayrı kusursuz olduğu bir sistemde, uçların birleşimi bir isteği hiç beklenmedik bir yanıtla karşılayabilir. Sonraki ders ödünç servisini gerçekten ayağa kaldırır, uçlarına istek gönderir ve tek bir soruyu ölçer: çalışan uygulamayı sınayan bir test uçların ne kadarına erişebiliyor ve bu iki yöntemin gördüğü kusur sınıfları nerede ayrışıyor.

İ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