---
title: 'Hesap Kurtarma'
source: 'https://academia.sh/tr/kurslar/kimlik-ve-yetki/hesap-kurtarma'
course: 'Kimlik Doğrulama ve Yetkilendirme'
language: tr
updated: '2026-08-17T18:06:49+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

Ö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.

```bash
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
```

```text
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.

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

const UYELER = new Map([["ayse@ornek.gecersiz", "U-1001"], ["mert@ornek.gecersiz", "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 ayse@ornek.gecersiz
echo "kayitsiz adres:";  dene /kusurlu/sifirlama kimse@ornek.gecersiz

echo "--- dogru kurulum ---"
echo "kayitli adres:";   dene /sifirlama ayse@ornek.gecersiz
echo "kayitsiz adres:";  dene /sifirlama kimse@ornek.gecersiz

kill $sunucu
rm -f kurtarma-sunucu.mjs
```

```text
--- 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ı.
