İçeriğe geç
academia.sh

Ders 09 / 16

Gizli Değerlerin Ayrılması

Yapılandırma ile gizli değer arasındaki sınır çıkış yollarına göre çizilir; anahtar adına ve değerin kendisine dayanan iki katlı maskeleme uygulanır, iç ayrıntının kütükte kalıp istemciye geçmemesi sağlanır ve bir sızıntı denetimi maskelenmemiş alanı çıkış koduyla yakalar.

İçindekiler

Önceki dersin şemasında bir değişken kasten yoktu: deponun parolası. Bağlantı noktası ile parola aynı yerden, ortamdan okunur ve aynı nesnede taşınır; buna karşılık biri açılışta kütüğe yazılabilir, diğeri yazılamaz. Bu ders o sınırı çizer ve sınırın kod içinde nerede uygulandığını gösterir.

Konu, gizli bir değeri saklamak değildir — değer zaten süreç belleğindedir ve uygulama onu kullanmak zorundadır. Konu, o değerin sürecin çıkış yollarına ulaşmasını engellemektir: kütük satırları, hata yanıtları, istemciye dönen gövdeler ve başlıklar.

Sınır Nerede

Yapılandırma ile gizli değeri ayıran ölçüt, değerin ne olduğu değil, açığa çıktığında ne olacağıdır. Bağlantı noktasının kütükte görünmesi bir sorun değildir; parolanın görünmesi, kütüğü okuyabilen herkesin depoya erişebilmesi anlamına gelir.

Bu ölçüt üç pratik kural üretir:

  • Gizli değer koda yazılmaz. Sürüm denetimindeki bir dosyaya giren değer, o deponun bütün kopyalarına ve bütün geçmişine girmiştir. Değiştirilmesi gerektiğinde geçmişten silinemez.
  • Gizli değer kütüğe yazılmaz. Kütükler biriktirilir, taşınır ve genelde uygulamanın kendisinden daha geniş bir kitleye açıktır.
  • Gizli değer istemciye ulaşmaz. Hata yanıtı, ayar görünümü ve tanı çıktıları bunun en sık kırıldığı yerlerdir.

Kurallar niyet olarak yeterli değildir; koda bağlanmaları gerekir. Aşağıdaki iki modül bunu yapar.

İki Katlı Maskeleme

Maskeleme (redaction), bir değeri çıktıya yazmadan önce sabit bir imle değiştirmektir. Uygulanmasının iki yolu vardır ve ikisi de tek başına eksiktir.

Birinci yol anahtar adına bakar: alan adı parola, anahtar, jeton gibi bir sözcük içeriyorsa değeri maskele. Bu yol bildirimseldir ve okunaklıdır, ama gizli değer beklenmedik bir alana düştüğünde kaçırır — örneğin bir hata iletisinin içine gömülmüş bağlantı adresinde.

İkinci yol değerin kendisine bakar: açılışta kaydedilmiş gizli değerlerin herhangi birine rastlanırsa, hangi alanda olursa olsun, değiştir. Bu yol beklenmedik yolları kapatır, ama yalnız açılışta kaydedilen değerleri tanır.

İkisi birlikte uygulanır.

// kaynak/kurulum/gizli.mjs — gizli degerleri kaydeder; kutuk ve yanit yollarinda maskeler
export const MASKE = "***";

const GIZLI_DEGERLER = new Set();
const GIZLI_ANAHTAR = /parola|anahtar|jeton|gizli|authorization|cookie/i;

// Acilista her gizli degerin bir kez kaydedilmesi, deger tabanli maskelemenin kaynagidir.
export const gizliKaydet = (deger) => {
  if (typeof deger === "string" && deger.length >= 6) GIZLI_DEGERLER.add(deger);
};

export const maskele = (deger, anahtar = "") => {
  if (typeof deger === "string") {
    if (GIZLI_ANAHTAR.test(anahtar)) return MASKE;              // 1. anahtar adina gore
    let sonuc = deger;
    for (const gizli of GIZLI_DEGERLER) sonuc = sonuc.split(gizli).join(MASKE);
    return sonuc;                                               // 2. degerin kendisine gore
  }
  if (Array.isArray(deger)) return deger.map((e) => maskele(e));
  if (deger instanceof Error) {
    return { ad: deger.name, ileti: maskele(deger.message), kod: deger.kod ?? null };
  }
  if (deger && typeof deger === "object") {
    return Object.fromEntries(Object.entries(deger).map(([a, d]) => [a, maskele(d, a)]));
  }
  return deger;
};

Hata nesnesinin ayrı ele alınması gerekir, çünkü JSON.stringify bir Error nesnesini boş nesneye çevirir; ileti ve yığıt izi kaybolur. Buradaki dönüşüm iletiyi korur ama maskeden geçirir.

Yapılandırma okuyucusu, gizli alanları okurken aynı zamanda kaydeder. Bu tek satır, değer tabanlı maskelemenin dayandığı kayıttır.

// kaynak/kurulum/ayar.mjs — ortamdan okur; gizli alanlari isaretler ve gizli kutugune kaydeder
import { gizliKaydet } from "./gizli.mjs";

const SEMA = {
  BAGLANTI_NOKTASI: { varsayilan: "8436" },
  DEPO_ADRESI: { varsayilan: "depo://[email protected]:5432/odunc" },
  DEPO_PAROLASI: { zorunlu: true, gizli: true },
  IMZALAMA_ANAHTARI: { zorunlu: true, gizli: true },
};

export const ayariOku = (ortam) => {
  const ayar = {}, eksik = [];
  for (const [ad, kural] of Object.entries(SEMA)) {
    const deger = ortam["KUTUPHANE_" + ad] ?? kural.varsayilan;
    if (deger === undefined) { eksik.push("KUTUPHANE_" + ad); continue; }
    if (kural.gizli) gizliKaydet(deger);
    ayar[ad] = deger;
  }
  if (eksik.length > 0) {
    console.error("yapilandirma gecersiz: " + eksik.join(", "));
    process.exit(78);
  }
  return Object.freeze(ayar);
};

Şemadaki gizli bayrağı, önceki dersin şemasına eklenen tek alandır. Ayrım şemada tanımlanır, uygulanması ise tek bir yerde toplanır.

Çıkış Yollarını Kapatmak

Sunucunun kuralı şudur: kütüğe ve yanıta giden her nesne maskeden geçer. Bu, iki fonksiyonun tek noktada tanımlanmasıyla sağlanır; başka hiçbir yerde doğrudan yazma yapılmaz.

// kaynak/http/sunucu.mjs — her cikis yolu maskeden gecer: kutuk, hata yaniti ve ayar gorunumu
import { createServer } from "node:http";
import { ayariOku } from "../kurulum/ayar.mjs";
import { maskele } from "../kurulum/gizli.mjs";

const ayar = ayariOku(process.env);
let sayac = 0;

const kutukle = (kayit) => console.log(JSON.stringify(maskele(kayit)));

const baglan = () => {                       // depoya baglanma denemesi: hata iletisi adresi tasir
  const hata = new Error(
    `depoya baglanilamadi: ${ayar.DEPO_ADRESI} (parola: ${ayar.DEPO_PAROLASI})`);
  hata.kod = "depo_erisilemez";
  throw hata;
};

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const kimlik = `i-${++sayac}`;
  const yol = new URL(istek.url, "http://yerel").pathname;
  const yaz = (durum, govde) => {
    const metin = JSON.stringify(maskele(govde));
    yanit.writeHead(durum, { "content-type": "application/json; charset=utf-8",
      "content-length": Buffer.byteLength(metin), "x-istek-kimligi": kimlik });
    yanit.end(metin);
  };

  try {
    if (yol === "/saglik") return yaz(200, { durum: "ayakta" });
    if (yol === "/ayarlar") return yaz(200, { ayar });          // gizli alanlar maskelenir
    if (yol === "/odunc") { baglan(); return; }
    yaz(404, { hata: "yol_yok", istekKimligi: kimlik });
  } catch (hata) {
    kutukle({ duzey: "hata", istekKimligi: kimlik, yol, hata });  // ic ayrinti yalniz kutukte
    yaz(500, { hata: "ic_hata", istekKimligi: kimlik });          // istemciye yalniz kod
  }
}).listen(Number(ayar.BAGLANTI_NOKTASI), "127.0.0.1",
  () => kutukle({ duzey: "bilgi", olay: "acildi", ayar }));

baglan fonksiyonu gerçek bir kazayı taklit eder: depoya bağlanan katmanların ürettiği hata iletileri sık sık bağlantı adresini, bazen de kimlik bilgisini taşır. Bu ileti maskeden geçmeden kütüğe yazılırsa gizli değer kütükte kalır.

Aynı catch bloğu ikinci bir ayrım daha kurar: iç ayrıntı kütüğe, istemciye yalnız bir hata kodu ve istek kimliği gider. Kimliğin iki tarafta da bulunması, destek isteyen bir üyenin verdiği numarayla kütükteki satırın bulunmasını sağlar; ayrıntının kendisi yanıta hiç geçmez.

Sızıntının Denetimi

Yukarıdaki kuralların uygulandığı, okunarak değil çalıştırılarak doğrulanır. Aşağıdaki program sunucuyu bilinen gizli değerlerle açar, uçlara istek yapar, yanıt gövdelerini, yanıt başlıklarını ve sürecin yazdığı bütün kütüğü toplar, sonra bu metinlerde gizli değerleri arar.

// denetim/sizinti.mjs — verilen sunucuyu bilinen gizli degerlerle acar; bu degerlerin yanit
// govdesine, yanit basliklarina ya da kutuge dusup dusmedigini sinar. Bulgu varsa 1 ile cikar.
// Kullanim: node denetim/sizinti.mjs <betik> <baglanti-noktasi>
import { spawn } from "node:child_process";
import { setTimeout as bekle } from "node:timers/promises";

const GIZLILER = {
  KUTUPHANE_DEPO_PAROLASI: "raf-parolasi-9f3a2b4c",
  KUTUPHANE_IMZALAMA_ANAHTARI: "imza-anahtari-7c1d8e5a",
};
const UCLAR = ["/saglik", "/ayarlar", "/odunc"];

const [betik, baglantiNoktasi] = process.argv.slice(2);

const cocuk = spawn("node", [betik], {
  env: { ...process.env, ...GIZLILER, KUTUPHANE_BAGLANTI_NOKTASI: baglantiNoktasi },
});
let kutuk = "";
cocuk.stdout.on("data", (p) => (kutuk += p));
cocuk.stderr.on("data", (p) => (kutuk += p));
await bekle(800);

const bulgular = [];
console.log(`### ${betik} — istemciye donen yanitlar`);
for (const yol of UCLAR) {
  const yanit = await fetch(`http://127.0.0.1:${baglantiNoktasi}${yol}`);
  const govde = await yanit.text();
  const basliklar = [...yanit.headers].map(([a, d]) => `${a}: ${d}`).join("\n");
  console.log(`  ${yanit.status} ${yol}  ${govde}`);
  for (const [ad, deger] of Object.entries(GIZLILER)) {
    if (govde.includes(deger)) bulgular.push(`${yol} yanit govdesi <- ${ad}`);
    if (basliklar.includes(deger)) bulgular.push(`${yol} yanit basliklari <- ${ad}`);
  }
}

await bekle(200);
cocuk.kill();
await bekle(300);
for (const [ad, deger] of Object.entries(GIZLILER)) {
  if (kutuk.includes(deger)) bulgular.push(`kutuk <- ${ad}`);
}

console.log("--- kutuk ---");
console.log(kutuk.trimEnd().split("\n").map((s) => "  " + s).join("\n"));
console.log("--- denetim ---");
for (const bulgu of bulgular) console.log(`  SIZINTI  ${bulgu}`);
console.log(bulgular.length === 0
  ? "  gizli deger hicbir cikis yolunda bulunmadi"
  : `  ${bulgular.length} sizinti bulundu`);
process.exit(bulgular.length === 0 ? 0 : 1);

Hiçbir sızıntı bulamayan bir denetim, iki nedenden dolayı sessiz kalabilir: gerçekten sızıntı yoktur ya da denetim bozuktur. İkisini ayırmak için denetimin bilerek kusurlu bırakılmış bir örnek üzerinde de çalıştırılması gerekir. Aşağıdaki dosya bu amaçla denetim/ dizininde durur — kaynak ağacının dışındadır ve uygulamanın hiçbir yolundan çağrılmaz.

// denetim/kusurlu-ornek.mjs — DENETIM ORNEGI, uretim kodu degildir.
// Maskeleme uygulanmamis bir surumu taklit eder; sizinti denetiminin gercekten
// yakaladigini kanitlamak icin durur ve kaynak agacinin disindadir.
import { createServer } from "node:http";
import { ayariOku } from "../kaynak/kurulum/ayar.mjs";

const ayar = ayariOku(process.env);

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const yol = new URL(istek.url, "http://yerel").pathname;
  const govde = yol === "/ayarlar" ? { ayar } : { durum: "ayakta" };
  const metin = JSON.stringify(govde);
  yanit.writeHead(200, { "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(metin) });
  yanit.end(metin);
}).listen(Number(ayar.BAGLANTI_NOKTASI), "127.0.0.1",
  () => console.log(JSON.stringify({ olay: "acildi", ayar })));
#!/usr/bin/env bash
# Sizinti denetimini once gercek sunucuya, sonra bilerek kusurlu birakilmis ornege uygular.
node denetim/sizinti.mjs kaynak/http/sunucu.mjs 8436; echo "cikis kodu=$?"
echo
node denetim/sizinti.mjs denetim/kusurlu-ornek.mjs 8437; echo "cikis kodu=$?"
### kaynak/http/sunucu.mjs — istemciye donen yanitlar
  200 /saglik  {"durum":"ayakta"}
  200 /ayarlar  {"ayar":{"BAGLANTI_NOKTASI":"8436","DEPO_ADRESI":"depo://[email protected]:5432/odunc","DEPO_PAROLASI":"***","IMZALAMA_ANAHTARI":"***"}}
  500 /odunc  {"hata":"ic_hata","istekKimligi":"i-3"}
--- kutuk ---
  {"duzey":"bilgi","olay":"acildi","ayar":{"BAGLANTI_NOKTASI":"8436","DEPO_ADRESI":"depo://[email protected]:5432/odunc","DEPO_PAROLASI":"***","IMZALAMA_ANAHTARI":"***"}}
  {"duzey":"hata","istekKimligi":"i-3","yol":"/odunc","hata":{"ad":"Error","ileti":"depoya baglanilamadi: depo://[email protected]:5432/odunc (parola: ***)","kod":"depo_erisilemez"}}
--- denetim ---
  gizli deger hicbir cikis yolunda bulunmadi
cikis kodu=0

### denetim/kusurlu-ornek.mjs — istemciye donen yanitlar
  200 /saglik  {"durum":"ayakta"}
  200 /ayarlar  {"ayar":{"BAGLANTI_NOKTASI":"8437","DEPO_ADRESI":"depo://[email protected]:5432/odunc","DEPO_PAROLASI":"raf-parolasi-9f3a2b4c","IMZALAMA_ANAHTARI":"imza-anahtari-7c1d8e5a"}}
  200 /odunc  {"durum":"ayakta"}
--- kutuk ---
  {"olay":"acildi","ayar":{"BAGLANTI_NOKTASI":"8437","DEPO_ADRESI":"depo://[email protected]:5432/odunc","DEPO_PAROLASI":"raf-parolasi-9f3a2b4c","IMZALAMA_ANAHTARI":"imza-anahtari-7c1d8e5a"}}
--- denetim ---
  SIZINTI  /ayarlar yanit govdesi <- KUTUPHANE_DEPO_PAROLASI
  SIZINTI  /ayarlar yanit govdesi <- KUTUPHANE_IMZALAMA_ANAHTARI
  SIZINTI  kutuk <- KUTUPHANE_DEPO_PAROLASI
  SIZINTI  kutuk <- KUTUPHANE_IMZALAMA_ANAHTARI
  4 sizinti bulundu
cikis kodu=1

Çıktının en öğretici satırı kütükteki hata kaydıdır. parola: *** yazan bu ileti, hiçbir alanı “parola” adını taşımadığı hâlde maskelenmiştir. Gizli değer bir hata iletisinin ortasına gömülüdür; onu yakalayan, açılışta kaydedilmiş değere göre çalışan ikinci kattır. Yalnız anahtar adına bakan bir maskeleme bu satırı olduğu gibi yazardı.

/ayarlar yanıtı ikinci ayrımı gösterir: aynı nesnedeki DEPO_ADRESI görünür, iki gizli alan *** olmuştur. Ayar görünümünün var olması bir sorun değildir; hangi alanların göründüğünün şemada tanımlanmış olması onu güvenli kılar.

/odunc yanıtı üçüncüsünü verir. İstemci {"hata":"ic_hata","istekKimligi":"i-3"} almıştır; deponun adresi, bağlantının neden kurulamadığı ve hatanın iç kodu yanıtta yoktur. Aynı isteğin kütük satırı bunların hepsini taşır ve iki kayıt i-3 ile eşleşir.

Kusurlu örnek üzerindeki çalıştırma denetimin boş bir onay üretmediğini kanıtlar: dört ayrı çıkış noktası adıyla bildirilmiş ve çıkış kodu 1 olmuştur. Bir yapı adımına bağlandığında bu kod, maskelemesi unutulmuş bir alanın yayına çıkmasını durdurur.

Değişimin Maliyeti

Maskeleme tek başına yeterli değildir, çünkü açığa çıkmış bir gizli değerin tek çaresi değiştirilmesidir. Uygulamanın bunu ucuz kılması gerekir: değer yalnız ortamdan okunuyorsa değişim, ortamı güncelleyip süreçleri yeniden başlatmaktan ibarettir. Aynı değer koda gömülüyse ya da birden çok yere kopyalanmışsa, değişim bir arama işine dönüşür.

Bunun ölçülebilir karşılığı şudur: gizli bir değerin uygulamada kaç yerde okunduğu sayılabilir olmalıdır. Yukarıdaki iskelette bu sayı birdir — ayariOku fonksiyonu. Değeri kullanan her kod parçası onu yapılandırma nesnesinden alır; hiçbiri process.env değerine kendi başına bakmaz.

Özet

  • Yapılandırma ile gizli değeri ayıran ölçüt, değerin açığa çıkması hâlinde doğacak sonuçtur; gizli değer koda yazılmaz, kütüğe yazılmaz ve istemciye ulaşmaz.
  • Maskeleme iki katlıdır: anahtar adına bakan kat bildirimsel ama eksiktir, açılışta kaydedilmiş değere bakan kat beklenmedik alanları kapatır; ölçümde hata iletisine gömülü parolayı ikinci kat yakalamıştır.
  • Hata nesneleri ayrı ele alınır, çünkü doğrudan dizileştirildiklerinde ileti kaybolur; dönüşüm iletiyi korur ve maskeden geçirir.
  • İç ayrıntı kütükte kalır, istemciye yalnız hata kodu ve istek kimliği gider; aynı kimlik iki kayıtta bulunduğu için sonradan eşleştirme yapılabilir.
  • Sızıntı denetimi sunucuyu bilinen gizli değerlerle açar, yanıtları ve kütüğü tarar; gerçek sunucuda 0, bilerek kusurlu bırakılmış örnekte dört bulgu ve 1 üretmiştir.

Sonraki Adım

Bu dersin kütük satırları JSON biçimindeydi ve her satır bir istek kimliği taşıyordu, ama bu kimlik yalnız iki yerde elle yazıldı. Bir istek zincirin beş halkasından ve üç modülden geçtiğinde her satırın aynı kimliği taşıması nasıl sağlanır? Düzey eşiği ne işe yarar ve bir isteğin bütün satırları sonradan hangi sorguyla toplanır? Sonraki ders yapılandırılmış günlüklemeyi kurar: istek bağlamını zincir boyunca taşır ve tek bir isteğin bütün kayıtlarını tek komutla toplar.

İ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