Ders 04 / 16
Güvenlik Başlıkları ve TLS
Taşıma katmanı sertleştirmesinin sunucu tarafındaki ölçüsü: altı güvenlik başlığının on uçta kaç kayıt noktasında durduğu, tek katmana taşımanın dokunduğu on bir dosya, eksik başlık ile yazılmış ama etkisiz değerin hata üretmeden düşürdüğü yükümlülük sayısı ve aktarılan protokol başlığının üç ayar altında ürettiği yanlış geçme ile yanlış kalma.
İçindekiler
Önceki ders sunucunun kaynaklar arası paylaşımdaki tek sözünün yanıt başlıkları olduğunu ve asıl sorunun başlığın hangi katmanda üretildiği olduğunu gösterdi. Bu ders aynı soruyu yanıtın taşıdığı öteki başlıklara taşıyor ve üç şey ölçüyor: bu başlıkların kaç uçta tekrarlandığı ve tek katmana taşımanın kaç dosyaya dokunduğu; eksik ya da yanlış yazılmış bir başlığın hata üretmeden ne düşürdüğü; uygulamanın “bu bağlantı şifreli” varsayımını nereden okuduğu ve o kaynak taklit edilebildiğinde ne olduğu.
Başlığın Sözleşmesi
Bir güvenlik başlığı, sunucunun uyumlu bir istemciye verdiği tek cümlelik bir yükümlülüktür; yazılmadığında ortada bir hata değil, bir yükümlülüğün yokluğu vardır. Ölçülen şey cümlenin kendisidir: yazılmış mı, yazılmışsa değeri o cümleyi gerçekten kuruyor mu.
// sertlestirme.mjs — baslik kumesi const tanim = (deger, eksikse, kural) => ({ deger, eksikse, etkili: (v) => kural.test(v.trim()) }); export const KUME = { "strict-transport-security": tanim("max-age=31536000; includeSubDomains", "ilk istek sifresiz denenebilir", /max-age=\s*[1-9]\d*/), "content-security-policy": tanim("default-src 'self'; frame-ancestors 'none'", "kaynak ve cerceve kisiti yok", /default-src(?![^;]*\*)/), "x-content-type-options": tanim("nosniff", "icerik turu tahmin edilebilir", /^nosniff$/i), "referrer-policy": tanim("no-referrer", "adres ve sorgu dizesi disariya tasinir", /^(no-referrer|same-origin|strict-origin)$/i), "x-frame-options": tanim("DENY", "gomulme kisiti yok", /^(deny|sameorigin)$/i), "permissions-policy": tanim("geolocation=(), camera=()", "yetenek kisiti yok", /=\(\)/), }; export function denetle(b) { const eksik = [], etkisiz = []; for (const [ad, k] of Object.entries(KUME)) { if (b[ad] === undefined) eksik.push(ad); else if (!k.etkili(b[ad])) etkisiz.push(ad); } return { eksik, etkisiz, saglam: Object.keys(KUME).length - eksik.length - etkisiz.length }; }
etkili alanı dersin ikinci ölçüsünü taşıyor: bir başlık yok, var ve etkili ya da var ama
etkisiz olabilir. Üçüncüsü ayrı sayılır, çünkü yanıta bakıldığında ikinciyle karışır — başlık
listede görünür, adı doğrudur, yalnız değeri hiçbir yükümlülük kurmaz.
BG6 (varsayım): istemcinin yükümlülüğü sözleşme olarak yazıldı, tarayıcı davranışı olarak
değil; web tarafı Frontend Kalitesi kursunda kuruldu. Ölçülen şey yanıtın kendisidir: node:http
ile okunan başlıklar.
Kaç Uçta Tekrarlanıyor
Ödünç sistemi dört servise bölünmüş ve dışarıya on uç veriyor. Başlıklar bugün her uç işleyicisinin içinde ayrı ayrı yazılıyor.
// sistem.mjs — uclar ve iki sapma export const SERVIS = { katalog: ["ara", "kitap", "oneri"], odunc: ["al", "uzat", "iade"], uye: ["profil", "ceza"], rapor: ["gunluk", "aylik"], }; export const UCLAR = Object.entries(SERVIS).flatMap(([s, us]) => us.map((u) => `/${s}/${u}`)); export const EKSIK = { "/rapor/aylik": 2 }; // son iki baslik yazilmamis export const YANLIS = { "/odunc/uzat": ["x-content-type-options", "no-sniff"] };
// dagilim.mjs — kayit noktasi ve tasima import { mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from "node:fs"; import { KUME } from "./sertlestirme.mjs"; import { UCLAR, EKSIK, YANLIS } from "./sistem.mjs"; const D = new URL("./uc/", import.meta.url).pathname; rmSync(D, { recursive: true, force: true }); mkdirSync(D); // her uc kendi yaziyor for (const uc of UCLAR) { const [yh, yv] = YANLIS[uc] ?? []; const satir = Object.entries(KUME) .slice(0, Object.keys(KUME).length - (EKSIK[uc] ?? 0)) .map(([h, k]) => ` y.setHeader(${JSON.stringify(h)}, ${JSON.stringify(h === yh ? yv : k.deger)});`); writeFileSync(D + uc.slice(1).replace("/", "-") + ".mjs", `export function isle(q, y) {\n${satir.join("\n")}\n y.end(JSON.stringify({ uc: ${JSON.stringify(uc)} }));\n}\n`); } const tara = () => readdirSync(D).map((f) => { const eslesme = [...readFileSync(D + f, "utf8").matchAll(/setHeader\("([^"]+)",\s"([^"]*)"\)/g)]; return { f, kayit: eslesme.length, saglam: eslesme.filter(([, h, v]) => KUME[h]?.etkili(v)).length }; }); const once = tara(); const tam = Object.keys(KUME).length; console.log(`${once.length} uc dosyasi, ${tam} baslik`); console.log(`kayit noktasi: ${once.length} kayit satiri: ${once.reduce((t, d) => t + d.kayit, 0)}`); console.log(`kumesi tam ve etkili olan dosya: ${once.filter((d) => d.saglam === tam).length}/${once.length}`); for (const d of once.filter((d) => d.saglam !== tam)) console.log(` ${d.f.padEnd(16)} kayit ${d.kayit}, etkili ${d.saglam}`); // tasima: satirlar silinir, katman yazilir let dokunulan = 0; for (const { f } of once) { const eski = readFileSync(D + f, "utf8"), yeni = eski.replace(/^\s*y\.setHeader\(.*\n/gm, ""); if (yeni !== eski) { writeFileSync(D + f, yeni); dokunulan++; } } const katman = Object.entries(KUME) .map(([h, k]) => ` y.setHeader(${JSON.stringify(h)}, ${JSON.stringify(k.deger)});`).join("\n"); writeFileSync(D + "katman.mjs", `export function sertlestir(q, y) {\n${katman}\n}\n`); dokunulan++; const sonra = tara().filter((d) => d.f !== "katman.mjs"); console.log(`\ntasimada dokunulan dosya: ${dokunulan} (${once.length} uc + 1 katman)`); console.log(`tasima sonrasi kayit noktasi: 1 uclarda kalan kayit satiri: ${sonra.reduce((t, d) => t + d.kayit, 0)}`); console.log(`yeni bir uc eklendiginde yazilacak baslik satiri: uc basina ${tam}, tek katmanda 0`);
10 uc dosyasi, 6 baslik kayit noktasi: 10 kayit satiri: 58 kumesi tam ve etkili olan dosya: 8/10 odunc-uzat.mjs kayit 6, etkili 5 rapor-aylik.mjs kayit 4, etkili 4 tasimada dokunulan dosya: 11 (10 uc + 1 katman) tasima sonrasi kayit noktasi: 1 uclarda kalan kayit satiri: 0 yeni bir uc eklendiginde yazilacak baslik satiri: uc basina 6, tek katmanda 0
On uç, on kayıt noktası. Altmış olması gereken kayıt satırı elli sekiz: /rapor/aylik iki
başlık geride kalmış. /odunc/uzat ucunda ise değer no-sniff yazılmış — başlık orada, adı
doğru, değeri tanımsız bir belirteç. On dosyanın sekizi tam. İki sapmanın da nedeni kayıt noktası
sayısıdır: altı başlık on ayrı yerde yazıldığında sapma için on fırsat vardır ve hiçbiri kendini
göstermez.
Taşımanın maliyeti tek seferliktir: on bir dosya. Sonrasında kayıt noktası bir, on birinci uç eklendiğinde yazılacak başlık satırı sıfır; uç başına kayıtta ise her yeni uç altı satır ister.
Eksik Başlık ve Etkisiz Değer
Kaç yerde yazıldığı dosyalarda ölçüldü; yanıtta ne göründüğü ayrı bir sorudur ve gerçek bir süreçten okunur.
// olcum.mjs — yanit basliklari import http from "node:http"; import { KUME, denetle } from "./sertlestirme.mjs"; import { UCLAR, EKSIK, YANLIS } from "./sistem.mjs"; const tam = () => Object.fromEntries(Object.entries(KUME).map(([h, k]) => [h, k.deger])); const AYARLAR = [ ["uc basina kayit", (uc) => { const b = tam(), [yh, yv] = YANLIS[uc] ?? []; for (const h of Object.keys(b).slice(Object.keys(b).length - (EKSIK[uc] ?? 0))) delete b[h]; if (yh) b[yh] = yv; return b; }], ["tek katman", () => tam()], ["tek katman, yanlis deger", () => ({ ...tam(), "strict-transport-security": "max-age=0" })], ]; const baslat = (uret) => new Promise((c) => { const s = http.createServer((q, y) => { y.writeHead(200, { "content-type": "application/json", ...uret(q.url) }); y.end(JSON.stringify({ uc: q.url })); }); s.listen(0, "127.0.0.1", () => c(s)); }); const iste = (port, uc) => new Promise((c) => http.get({ host: "127.0.0.1", port, path: uc }, (y) => { y.resume(); c(y); })); console.log(`${UCLAR.length} uc, ${Object.keys(KUME).length} baslik, ayar basina ${UCLAR.length} istek\n`); console.log(`${"ayar".padEnd(25)}${"200".padStart(5)}${"hata".padStart(6)}${"bayt".padStart(7)}` + `${"sozlesmesi tam uc".padStart(19)}${"eksik".padStart(7)}${"etkisiz".padStart(9)}`); const ayrinti = []; for (const [ad, uret] of AYARLAR) { const s = await baslat(uret); let [iki, bayt, saglamUc, eksik, etkisiz] = [0, 0, 0, 0, 0]; for (const uc of UCLAR) { const y = await iste(s.address().port, uc); if (y.statusCode === 200) iki++; for (const h of Object.keys(KUME)) if (y.headers[h]) bayt += h.length + y.headers[h].length + 4; const d = denetle(y.headers); if (d.saglam === Object.keys(KUME).length) saglamUc++; else ayrinti.push([ad, uc, d]); eksik += d.eksik.length; etkisiz += d.etkisiz.length; } s.close(); console.log(ad.padEnd(25) + String(iki).padStart(5) + "0".padStart(6) + String(bayt).padStart(7) + `${saglamUc}/${UCLAR.length}`.padStart(19) + String(eksik).padStart(7) + String(etkisiz).padStart(9)); } console.log(`\nsozlesmesi eksilen uclar ve sessiz sonuc:`); for (const [ad, uc, d] of ayrinti.slice(0, 4)) { const not = ["eksik", "etkisiz"].flatMap((t) => d[t].map((h) => `${t} ${h} -> ${KUME[h].eksikse}`)); console.log(` ${ad.padEnd(25)}${uc.padEnd(15)}${not.join("; ")}`); } console.log(` ... "tek katman, yanlis deger" ayarinda ayni satir ${UCLAR.length} ucun ${UCLAR.length} tanesinde tekrarlaniyor`);
10 uc, 6 baslik, ayar basina 10 istek ayar 200 hata bayt sozlesmesi tam uc eksik etkisiz uc basina kayit 10 0 2591 8/10 2 1 tek katman 10 0 2660 10/10 0 0 tek katman, yanlis deger 10 0 2400 0/10 0 10 sozlesmesi eksilen uclar ve sessiz sonuc: uc basina kayit /odunc/uzat etkisiz x-content-type-options -> icerik turu tahmin edilebilir uc basina kayit /rapor/aylik eksik x-frame-options -> gomulme kisiti yok; eksik permissions-policy -> yetenek kisiti yok tek katman, yanlis deger /katalog/ara etkisiz strict-transport-security -> ilk istek sifresiz denenebilir tek katman, yanlis deger /katalog/kitap etkisiz strict-transport-security -> ilk istek sifresiz denenebilir ... "tek katman, yanlis deger" ayarinda ayni satir 10 ucun 10 tanesinde tekrarlaniyor
Otuz isteğin otuzu 200 döndü; hata sayısı üç ayarda da sıfır. Tek nicel fark sertleştirme baytındadır: 2591, 2660, 2400 — ve en az bayt üreten ayar, sözleşmesi on uçtan sıfırında tam olan ayardır. Yanıt boyunu izleyen bir ölçüm bu üç ayarı doğruluk sırasına koyamaz.
uc basina kayit dosyalardaki sapmayı yanıta taşıyor: iki eksik yükümlülük, bir etkisiz değer,
sekiz sağlam uç. tek katman sapmayı sıfırlıyor. Üçüncü satır ise tek katmanın öbür yüzüdür:
başlık on ucun onunda var, adı doğru, biçimi geçerli, ama max-age sıfır olduğu için hiçbir
yükümlülük kurmuyor — tek yerdeki tek karakterlik fark sözleşmesi tam uç sayısını 10/10’dan
0/10’a düşürdü, durum kodu ve gövde değişmeden.
Bu, kayıt noktasını teke indirme kararını geri almaz; ölçünün nereye konacağını söyler. Tek
katman sapmayı bitirir ve değer riskini yoğunlaştırır; ikisini birden yakalayan tek şey
denetle gibi bir denetimin yapılandırmanın yanında değil yanıtın üzerinde çalışmasıdır.
Şifreli Bağlantı Varsayımı
Kümedeki ilk başlık ötekilerden bir yönüyle ayrılır: yalnız şifreli bir bağlantı üzerinde anlamlıdır ve uygulama bağlantının şifreli olduğunu doğrudan göremez. TLS sonlandırma uygulamanın kendisinde değil, önündeki ters vekilde yapılıyor; sonlandırmanın hangi katmanda durduğu ve maliyeti yük dengeleme kursunda ölçüldü, burada tekrarlanmıyor. Uygulamaya kalan tek bilgi vekilin aktardığı bir başlıktır.
BG7 (varsayım): yerel bağlantılar şifresizdir; vekil üzerinden gelen istek şifreli, doğrudan ulaşan şifresiz sayılır. Ölçülen şey uygulamanın bu gerçeği ne kadar doğru tahmin ettiğidir. BG8 (varsayım): üçüncü ayardaki yerel yuva, “uygulamaya ağdan yalnız vekil aracılığıyla erişilir” kararının ölçülebilir karşılığıdır.
// tasima.mjs — sifreli baglanti varsayimi import http from "node:http"; import { tmpdir } from "node:os"; import { rmSync } from "node:fs"; import { KUME } from "./sertlestirme.mjs"; const BASLIK = "x-forwarded-proto"; const yerel = (port) => ({ host: "127.0.0.1", port }); const ac = (s, adres) => new Promise((c) => s.listen(adres, () => c(s))); // uygulama: sifresizi yonlendirir const uygulama = (kural) => (q, y) => { const p = (q.headers[BASLIK] ?? "").split(",").map((s) => s.trim()).filter(Boolean) .at(kural === "ilk" ? 0 : -1); if (p !== "https") return y.writeHead(308, { location: "https://odunc.kutuphane" + q.url }).end(); y.writeHead(200, { "strict-transport-security": KUME["strict-transport-security"].deger }); y.end(JSON.stringify({ uc: q.url })); }; // vekil: sonlandirir ve aktarir function vekil(hedef, bicim) { const s = http.createServer((q, y) => { const gelen = q.headers[BASLIK], zincir = bicim === "ekleyen" && gelen ? `${gelen}, https` : "https"; const i = http.request({ ...hedef, path: q.url, headers: { ...q.headers, [BASLIK]: zincir } }, (c) => { y.writeHead(c.statusCode, c.headers); c.pipe(y); }); i.end(); }); return ac(s, yerel(0)); } const dinle = (kural, adres) => { if (typeof adres === "string") rmSync(adres, { force: true }); return ac(http.createServer(uygulama(kural)), adres); }; const iste = (hedef, gonderilen) => new Promise((c) => { const i = http.get({ ...hedef, path: "/odunc/al", headers: gonderilen ? { [BASLIK]: gonderilen } : {} }, (y) => { y.resume(); c(y); }); i.on("error", () => c({ statusCode: 0 })); }); // kapali oldugu bilinen bir TCP adresi const kapali = await ac(http.createServer(), yerel(0)); const kapaliPort = kapali.address().port; kapali.close(); const ISTEKLER = ["vekil", "dogrudan"].flatMap((y) => [null, "https", "http"].map((g) => [y, g])); const yuva = tmpdir() + "/odunc.sock"; const AYARLAR = [ ["ekleyen vekil, ilk deger", "ilk", "ekleyen", null], ["ekleyen vekil, son deger", "son", "ekleyen", null], ["ustune yazan vekil, yalniz yuva", "son", "yazan", yuva], ]; console.log(`${ISTEKLER.length} istek: 3'u vekil uzerinden (gercek: sifreli), 3'u dogrudan (gercek: sifresiz)\n`); console.log(`${"ayar".padEnd(34)}${"yanlis gecme".padStart(13)}${"yanlis kalma".padStart(14)}` + `${"tasima basligi/6".padStart(18)}${"ulasilamayan/3".padStart(16)}`); for (const [ad, kural, bicim, adres] of AYARLAR) { const app = await dinle(kural, adres ?? yerel(0)); const v = await vekil(adres ? { socketPath: adres } : yerel(app.address().port), bicim); let [gecme, kalma, tb, kopuk] = [0, 0, 0, 0]; for (const [yol, gonderilen] of ISTEKLER) { const sifreli = yol === "vekil"; const y = await iste(yerel(sifreli ? v.address().port : adres ? kapaliPort : app.address().port), gonderilen); if (y.statusCode === 0) { kopuk++; continue; } if (y.headers["strict-transport-security"]) tb++; if (!sifreli && y.statusCode === 200) gecme++; if (sifreli && y.statusCode === 308) kalma++; } console.log(ad.padEnd(34) + String(gecme).padStart(13) + String(kalma).padStart(14) + String(tb).padStart(18) + String(kopuk).padStart(16)); v.close(); app.close(); } // yanlis kalmanin bedeli const app = await dinle("ilk", yerel(0)); const v = await vekil(yerel(app.address().port), "ekleyen"); const durumlar = []; for (let i = 0; i < 5; i++) durumlar.push((await iste(yerel(v.address().port), "http")).statusCode); console.log(`\n"ilk deger" ayarinda sifreli ama ters baslik tasiyan istek: 5 deneme -> ${durumlar.join(", ")}`); console.log(`uygulamanin dondugu 5xx sayisi: 0; kayitta hata yok`); v.close(); app.close();
6 istek: 3'u vekil uzerinden (gercek: sifreli), 3'u dogrudan (gercek: sifresiz) ayar yanlis gecme yanlis kalma tasima basligi/6 ulasilamayan/3 ekleyen vekil, ilk deger 1 1 3 0 ekleyen vekil, son deger 1 0 4 0 ustune yazan vekil, yalniz yuva 0 0 3 3 "ilk deger" ayarinda sifreli ama ters baslik tasiyan istek: 5 deneme -> 308, 308, 308, 308, 308 uygulamanin dondugu 5xx sayisi: 0; kayitta hata yok
Üç ayar aynı altı isteği görüyor. İlk ayar zincirin ilk değerini okuyor ve o değeri istemci yazabiliyor: şifresiz gelen taklitçi istek kabul ediliyor (bir yanlış geçme), gerçekten şifreli olan bir istek geri çevriliyor (bir yanlış kalma). İkinci ayar zincirin son değerini, yani uygulamaya en yakın katmanın yazdığını okuyor; yanlış kalma sıfırlanıyor ama yanlış geçme duruyor — doğrudan gelen istekte zincirde tek değer vardır ve onu istemci yazmıştır, “son değer” kuralı orada ilk değeri okumakla aynı şeydir.
Üçüncü ayar iki şeyi birden değiştiriyor: vekil istemcinin gönderdiğini aktarmıyor, üstüne yazıyor; uygulama ağdan değil yalnız yerel bir yuvadan dinliyor. Doğrudan üç istek bağlantı kuramıyor, iki hata biçimi de sıfırlanıyor, taşıma başlığı tam olarak hak eden üç yanıta yazılıyor. Sonuç bir başlık kuralından çok bir yol kuralıdır: aktarılan başlığa güvenmek, o başlığı yazan katmanın tek giriş noktası olmasına güvenmektir.
Yanlış kalmanın bedeli de sessizdir. İlk ayarda ters başlık taşıyan şifreli istek beş denemenin beşinde 308 döndü; yönlendirmeyi izleyen istemci aynı isteği aynı başlıkla yeniden üretir ve aynı yanıtı alır. Uygulamanın döndüğü 5xx sayısı sıfırdır — kayıtta hata arayan biri bu döngüyü bulamaz. Aynı karar önceki bölümü de bağlıyor: taşıma başlığı yalnız uygulama bağlantıyı şifreli saydığında yazılıyor, ilk ayarda o üç yanıttan biri şifresiz bir bağlantıya gitti.
Özet
- Uç başına yazımda 10 kayıt noktası ve 58 kayıt satırı vardır; iki sapma 2 dosyada durdu. Tek katmana taşıma 11 dosyaya dokundu, kayıt noktasını 1’e, yeni uç için yazılacak satırı 0’a indirdi.
- Üç ayarda 30 isteğin 30’u 200 döndü, hata 0; sertleştirme baytı 2591 / 2660 / 2400 ve en az bayt üreten ayar sözleşmesi 0/10 uçta tam olandır — yanıt boyu doğruluk sırası vermez.
- Yazılmış ama etkisiz değer eksik başlıktan ayrı sayılır: tek katmandaki tek yanlış değer sözleşmesi tam uç sayısını 10/10’dan 0/10’a düşürdü, durum kodu ve gövde değişmeden. Denetim bu yüzden yapılandırmanın değil yanıtın üzerinde çalışır.
- Aktarılan protokol başlığında ilk değer kuralı 1 yanlış geçme ve 1 yanlış kalma üretti; son değer kuralı yalnız yanlış kalmayı sıfırladı, çünkü doğrudan gelen istekte zincirdeki tek değeri istemci yazar.
- Üstüne yazan vekil ve yalnız yerel yuvadan dinleme ikisini de sıfırladı: 3 doğrudan istek bağlantı kuramadı, başlık yalnız hak eden 3 yanıta yazıldı. Yanlış kalmanın izi yoktur — ters başlık taşıyan şifreli istek 5 denemenin 5’inde 308 döndü, 5xx sayısı 0.
Sonraki Adım
Taşıma tarafı bu dersle sertleşti: başlıklar tek katmanda toplandı, değerleri yanıt üzerinden denetlenebilir hâle geldi ve şifreli bağlantı varsayımı tek bir yola bağlandı. Ama bu derste kullanılan her değer kodun içine düz metin olarak yazıldı. Vekilin uygulamaya ulaştığı yol, denetimin beklediği değerler ve şifreli bağlantının dayandığı anahtarlar aynı yerde durur: kaynak dosyalarda. Sonraki ders bu değerleri sayar — kaçı kodda kalmış, biri değiştirildiğinde kaç servise dokunuluyor ve döndürme sırasında eski ile yeni değerin birlikte geçerli olduğu pencere kaç saniye sürüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.