İçeriğe geç
academia.sh

Ders 15 / 23

Hesap Kurtarma

Parola sıfırlama akışının saldırı yüzeyi: belirtecin rastgele üretilmesi, karma olarak saklanması, tek kullanımlık ve kısa ömürlü olması, kullanıcı numaralandırmasına karşı aynı yanıt ve sıfırlamanın oturumlar üzerindeki etkisi.

İçindekiler

Önceki ders parolanın nasıl seçileceğini kurdu. Bu ders, parolanın unutulduğu durumu ele alıyor — ve o durum, sistemin en dikkatli kurulmuş kimlik doğrulama akışını tümüyle atlayan ikinci bir kapıdır. Kütüphane üyesi parolasını hatırlamadığında, sistem onu parolasız tanımak zorundadır. Bu tanımanın nasıl yapıldığı, önceki üç dersteki tüm savunmaların üzerine kurulan bir sınırdır: kurtarma akışı zayıfsa, parolanın ne kadar iyi saklandığının önemi kalmaz.

Akışın iskeleti üç adımdır. Kullanıcı bir adres bildirir; sistem o adrese, yalnız o adrese ulaşabilen birinin eline geçecek bir değer gönderir; o değeri geri getiren kişi yeni parola belirler. Dersin tamamı ikinci adımdaki değerin — sıfırlama belirtecinin (reset token) — özellikleriyle, birinci adımdaki yanıtın ne söylediğiyle ilgilidir.

Belirtecin Dört Özelliği

Sıfırlama belirteci, kısa ömürlü bir kimlik bilgisidir ve parolayla aynı özeni gerektirir.

Rastgele üretilir. Kullanıcı numarasından, zaman damgasından ya da sıralı bir sayaçtan türetilen belirteç öngörülebilirdir. Kaynak, kriptografik rastgele sayı üretecidir ve uzunluk en az otuz iki bayttır.

Karma olarak saklanır. Veritabanında duran değer belirtecin kendisi değil, karmasıdır. Bunun gerekçesi parola karmasıyla aynıdır: veritabanını okuyabilen biri, açık bekleyen sıfırlama isteklerini kullanabilecek durumda olmamalıdır. Belirteç yüksek entropili olduğu için burada yavaş karma gerekmez; tek geçişli bir karma yeterlidir.

Tek kullanımlıktır. İlk kullanımda kapanır. Kapanmayan bir belirteç, e-posta kutusuna sonradan erişen birinin hesabı yeniden ele geçirmesine yeter.

Kısa ömürlüdür. On beş dakikalık bir pencere yerleşik seçimdir; ölçüt, kullanıcının e-postasını açıp bağlantıya tıklaması için gereken süredir. Uzun ömür, belirteci posta kutusunda bekleyen kalıcı bir anahtara çevirir.

Aşağıdaki betik bu dört özelliği bir arada kuruyor ve son ikisini sorguyla kanıtlıyor.

cat > kurtarma.mjs <<'EOF'
import { randomBytes, createHash } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync(":memory:");
db.exec(`CREATE TABLE sifirlama(
  karma TEXT PRIMARY KEY, uye TEXT NOT NULL,
  uretim INTEGER NOT NULL, bitis INTEGER NOT NULL, kullanim INTEGER)`);

const karmala = (b) => createHash("sha256").update(b).digest("hex");
const simdi = () => Date.now();
const OMUR_MS = 15 * 60 * 1000;                    // 15 dakika

function uret(uye, omur = OMUR_MS) {
  const belirtec = randomBytes(32).toString("base64url");
  db.prepare("INSERT INTO sifirlama VALUES (?,?,?,?,NULL)")
    .run(karmala(belirtec), uye, simdi(), simdi() + omur);
  return belirtec;                                  // yalniz baglantida gider
}

function kullan(belirtec) {
  const kayit = db.prepare("SELECT * FROM sifirlama WHERE karma = ?").get(karmala(belirtec));
  if (!kayit) return "gecersiz";
  if (kayit.kullanim !== null) return "zaten kullanilmis";
  if (simdi() > kayit.bitis) return "suresi dolmus";
  db.prepare("UPDATE sifirlama SET kullanim = ? WHERE karma = ? AND kullanim IS NULL")
    .run(simdi(), kayit.karma);
  return `kabul: ${kayit.uye}`;
}

const gecerli = uret("U-1001");
const eskimis = uret("U-1005", -1000);              // uretildigi anda suresi dolmus

console.log("uretilen belirtec (baglantida giden):", gecerli.slice(0, 16) + "...");
console.log("veritabaninda duran   :",
  db.prepare("SELECT karma FROM sifirlama WHERE uye='U-1001'").get().karma.slice(0, 16) + "...");
console.log("belirtec kayitta mi   :",
  db.prepare("SELECT COUNT(*) s FROM sifirlama WHERE karma = ?").get(gecerli).s > 0);

console.log("\nilk kullanim          :", kullan(gecerli));
console.log("ikinci kullanim       :", kullan(gecerli));
console.log("suresi dolmus belirtec:", kullan(eskimis));
console.log("bilinmeyen belirtec   :", kullan(randomBytes(32).toString("base64url")));

console.log("\nkayit durumu:");
for (const s of db.prepare(
  `SELECT uye, substr(karma,1,10) k, (bitis - uretim)/1000 omur_sn,
          CASE WHEN kullanim IS NULL THEN 'acik' ELSE 'kapali' END durum
   FROM sifirlama ORDER BY uye`).all()) {
  console.log(`  ${s.uye}  ${s.k}  omur=${s.omur_sn} sn  durum=${s.durum}`);
}
EOF
node kurtarma.mjs
uretilen belirtec (baglantida giden): c8l-YUVmIfuvctZL...
veritabaninda duran   : 788db50ac4677fe2...
belirtec kayitta mi   : false

ilk kullanim          : kabul: U-1001
ikinci kullanim       : zaten kullanilmis
suresi dolmus belirtec: suresi dolmus
bilinmeyen belirtec   : gecersiz

kayit durumu:
  U-1001  788db50ac4  omur=900 sn  durum=kapali
  U-1005  80c7c5ee03  omur=-1 sn  durum=acik

Belirteç değerleri her çalıştırmada değişir; satırların anlamı değişmez. Üçüncü satır saklama kararının doğrulamasıdır: belirtecin kendisiyle yapılan sorgu hiçbir kayıt bulmuyor, çünkü tabloda belirteç değil karması duruyor.

Sonraki dört satır belirtecin durum çizelgesini gösteriyor. İlk kullanım kabul edildi, ikincisi reddedildi, süresi geçmiş olan reddedildi, hiç üretilmemiş olan reddedildi. Bu dört sonucun her biri ayrı bir kod yoludur ve her biri ayrı sınanmalıdır; en sık atlanan ikincisidir.

Güncelleme deyimindeki AND kullanim IS NULL koşulu bir ayrıntı değildir. İki eşzamanlı istek aynı belirteci getirirse, koşulsuz bir güncelleme ikisini de kabul eder; koşullu güncellemede yalnız biri satırı değiştirir. İleri SQL kursundaki koşullu güncelleme kalıbının buradaki karşılığı budur.

Kullanıcı Numaralandırması

Akışın ilk adımı, kullanıcının bir adres bildirmesidir. Sunucunun bu isteğe verdiği yanıt, o adresin sistemde kayıtlı olup olmadığını söylememelidir. Söylerse, akış bir üyelik sorgusu aracına dönüşür: elindeki adres listesini tek tek deneyen biri, hangilerinin kütüphaneye üye olduğunu öğrenir. Buna kullanıcı numaralandırması (user enumeration) denir.

Sızan şey parola değildir, ama önemsiz de değildir. Bir kütüphane için üyelik bilgisi kişiseldir; başka alanlarda aynı sızıntı çok daha ağır sonuçlar doğurur.

Aşağıdaki blok iki kurulumu yan yana çalıştırıyor. Birincisi reddedilmesi gereken kurulumdur ve yalnız karşılaştırma için buradadır.

cat > kurtarma-sunucu.mjs <<'EOF'
import { createServer } from "node:http";
import { randomBytes, createHash, scryptSync } from "node:crypto";

const UYELER = new Map([["[email protected]", "U-1001"], ["[email protected]", "U-1005"]]);
const TABAN_MS = 120;                       // her iki dalda da harcanan taban sure

const govde = (istek) => new Promise((coz) => {
  let v = ""; istek.on("data", (p) => (v += p)); istek.on("end", () => coz(JSON.parse(v || "{}")));
});

function belirtecUret(uye) {                // gercek isin maliyeti: uretim + karma
  const b = randomBytes(32).toString("base64url");
  scryptSync(b, uye, 32, { N: 2 ** 12, r: 8, p: 1 });
  return createHash("sha256").update(b).digest("hex");
}

createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://yerel").pathname;
  if (yol === "/saglik") return yanit.writeHead(200).end("hazir\n");

  const { eposta } = await govde(istek);
  const uye = UYELER.get(eposta);

  if (yol === "/kusurlu/sifirlama") {        // reddedilmesi gereken kurulum
    if (!uye) return yanit.writeHead(404, { "content-type": "application/json" })
      .end(JSON.stringify({ hata: "kayitli_olmayan_adres" }) + "\n");
    belirtecUret(uye);
    return yanit.writeHead(200, { "content-type": "application/json" })
      .end(JSON.stringify({ durum: "gonderildi", uye }) + "\n");
  }

  const basla = Date.now();
  if (uye) belirtecUret(uye); else belirtecUret("yok");   // ayni is her iki dalda
  const kalan = TABAN_MS - (Date.now() - basla);
  setTimeout(() => {
    yanit.writeHead(202, { "content-type": "application/json" })
      .end(JSON.stringify({ durum: "istek alindi" }) + "\n");
  }, kalan > 0 ? kalan : 0);
}).listen(8494, "127.0.0.1");
EOF
node kurtarma-sunucu.mjs & sunucu=$!
until curl -sf http://127.0.0.1:8494/saglik > /dev/null; do :; done

dene() {
  curl -s -X POST "http://127.0.0.1:8494$1" -H 'content-type: application/json' \
    -d "{\"eposta\":\"$2\"}" -w '  HTTP %{http_code}  sure=%{time_total} sn\n'
}

echo "--- reddedilmesi gereken kurulum ---"
echo "kayitli adres:";   dene /kusurlu/sifirlama [email protected]
echo "kayitsiz adres:";  dene /kusurlu/sifirlama [email protected]

echo "--- dogru kurulum ---"
echo "kayitli adres:";   dene /sifirlama [email protected]
echo "kayitsiz adres:";  dene /sifirlama [email protected]

kill $sunucu
rm -f kurtarma-sunucu.mjs
--- reddedilmesi gereken kurulum ---
kayitli adres:
{"durum":"gonderildi","uye":"U-1001"}
  HTTP 200  sure=0.005558 sn
kayitsiz adres:
{"hata":"kayitli_olmayan_adres"}
  HTTP 404  sure=0.000523 sn
--- dogru kurulum ---
kayitli adres:
{"durum":"istek alindi"}
  HTTP 202  sure=0.121078 sn
kayitsiz adres:
{"durum":"istek alindi"}
  HTTP 202  sure=0.122348 sn

Süre değerleri makineye göre değişir; ayrışma örüntüsü değişmez. Reddedilmesi gereken kurulum üç ayrı kanaldan bilgi veriyor.

Birincisi durum kodudur: 200 ile 404, iki ayrı sonucu adlandırıyor. İkincisi gövdedir; kayıtlı adres için üye numarası bile dönüyor. Üçüncüsü süredir — kayıtlı adres için yapılan belirteç üretimi ölçülebilir bir gecikme bırakıyor, kayıtsız adres anında dönüyor. İlk ikisi düzeltilip üçüncüsü bırakılırsa sızıntı sürer; bu yüzden üçü birlikte ele alınır.

Doğru kurulumda üç kanal da kapalı: aynı kod, aynı gövde, aynı süre. Sunucu her iki dalda da aynı işi yapıyor ve yanıtı sabit bir taban süreye oturtuyor. Kod 202’dir, çünkü söylenen şey “istek alındı”dır; “gönderildi” demek, gönderilebildiği bilgisini vermektir.

Aynı kural akışın ikinci adımında da geçerlidir. Geçersiz, kullanılmış ve süresi dolmuş belirteçler için kullanıcıya gösterilen ileti tektir: “bu bağlantı artık geçerli değil, yeniden isteyin.” Ayrı iletiler, belirtecin hangi durumda olduğunu ve dolaylı olarak hesabın varlığını bildirir. Kütük kayıtlarında ise ayrım korunur; işletim tarafının hangi yolun çalıştığını bilmesi gerekir.

Belirtecin Taşınması

Belirteç bir bağlantı içinde taşınır ve bu taşımanın kendi kuralları vardır.

Adres, ziyaret edildiği anda referans başlığıyla üçüncü taraflara sızabilir. Sıfırlama sayfası dış kaynak yüklememeli ve referans politikası sıkı olmalıdır; İstemci Güvenliği konusunda tanımlanan referans politikası kararı burada doğrudan uygulanır.

İkinci kural kütüklerdedir. Adres bileşenleri sunucu erişim kütüklerine yazılır; belirteç sorgu bölümünde taşınıyorsa kütüklere de düşer. Sunucu Tarafı Temelleri kursunda kurulan maskeleme kuralı sıfırlama adreslerini de kapsamalıdır.

Üçüncüsü, sıfırlama sayfasının belirteci kalıcı olarak saklamamasıdır. Sayfa açıldığında belirteç bir kez okunur, yeni parola gönderilirken kullanılır ve adres çubuğundan temizlenir.

Sıfırlamanın Yan Etkileri

Başarılı bir sıfırlama yalnız parolayı değiştirmez.

Açık oturumlar sonlandırılır. Parolanın sıfırlanması, hesabın ele geçirilmiş olabileceği anlamına gelir; o hesaba ait tüm oturumların kapatılması, sıfırlama işleminin ayrılmaz parçasıdır. Bunun nasıl yapıldığı sonraki dersin konusudur.

Diğer açık sıfırlama belirteçleri kapatılır. Aynı hesap için birden çok istek yapılmış olabilir; kullanılmayanlar aynı işlemde geçersizleşir.

Kullanıcıya bilgi verilir. Parolanın değiştirildiği, kayıtlı adrese ayrıca bildirilir. Bu bildirim, işlemi kullanıcı yapmadıysa fark edilmesini sağlayan tek düzenektir.

Otomatik giriş yapılmaz. Sıfırlama sonrası kullanıcı, yeni parolasıyla normal giriş akışından geçer. Sıfırlama akışının doğrudan oturum açması, kurtarma kapısını giriş kapısıyla eşitler.

Son olarak, istek adımı hız sınırına (rate limiting) tabidir. Sınır hem adres başına hem kaynak adres başına konur; aksi hâlde akış, adres listesi denemek için ölçeklenebilir bir araç hâline gelir. Sınır aşıldığında dönen yanıt da ayrışmamalıdır — aynı 202, aynı gövde.

Özet

  • Sıfırlama belirteci rastgele üretilir, karma olarak saklanır, tek kullanımlıktır ve kısa ömürlüdür; dört özelliğin her biri ayrı bir kod yolunda sınanır.
  • Belirtecin karma olarak saklanması, veritabanını okuyabilen birinin açık sıfırlama isteklerini kullanmasını engeller.
  • Kullanıcı numaralandırması durum kodu, gövde ve süre olmak üzere üç kanaldan sızar; üçü birlikte kapatılmadıkça sızıntı sürer.
  • Belirteç bağlantıda taşınır; referans başlığı, erişim kütükleri ve adres çubuğu bu taşımanın sızıntı noktalarıdır.
  • Başarılı sıfırlama açık oturumları ve diğer belirteçleri kapatır, kullanıcıya bildirim gönderir ve otomatik giriş yapmaz.

Sonraki Adım

Bu dersin son kuralı bir sonraki dersin sorusunu doğuruyor: “hesaba ait tüm oturumları kapat” cümlesinin karşılığı nedir? Oturum kimliği tarayıcıda duruyorsa sunucu onu nasıl geçersizleştirir, kullanıcı hangi aygıtlardan girmiş olduğunu nereden görür? Sonraki ders oturum güvenliğine geçiyor: girişte oturum kimliğinin yenilenmesi, eşzamanlı oturumların listelenmesi ve tek tek sonlandırılması, boşta kalma ile mutlak ömür sınırlarının birlikte uygulanması.

İ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