IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi12/31-dars27 daqiqa
Mundarija (41)

Email tasdiqlash va parol tiklash oqimlari: bir martalik token, muddat va enumeration'siz javob

Qisqacha: Xatdagi havola — vaqtinchalik kalit. U kriptografik tasodifiy (32 bayt), bazada faqat hash'i turadi, qisqa muddatli (tiklash — 30 daqiqa) va bir marta ishlaydi — atomik UPDATE … RETURNING bilan. Javob email bor-yo'qligini na matni, na vaqti bilan oshkor qilmasligi kerak: kanon bir xil matnli javob vaqtda farq qilishini o'lchab topdi (30.0 va 12.8 ms) va xat yuborishni fonga oldi. Parol tiklangandan keyin foydalanuvchining hamma sessiyalari bekor qilinadi.

Bu darsda

  • Bir martalik tokenning to'rt qoidasini va nega bazada hash saqlanishini tushuntirasiz.
  • Tokenni havolaning # qismiga qo'yish va havolani Host sarlavhasidan yasamaslik sababini ko'rasiz.
  • Email enumeration'ni javob matni va javob vaqti orqali qanday yopishni o'rganasiz.
  • Tasdiqlanmagan hisob, pre-hijacking va "tiklangandan keyin hammasidan chiqish" qoidalarini yozasiz.
  • vazifalar-api ga 114-migratsiya, token.ts, fon.ts, to'rtta yo'l va yangi testlarni qo'shasiz.

Oldin bilishingiz kerak: crypto 1: tasodif, hash va HMAC, Ro'yxat va login oqimi hamda hisob xavfsizligi, Session-based auth, Session vs JWT qarori va uzoq sessiyalar, Email yuborish: SMTP va Nodemailer.

Oldingi darsda sessiyalarning umrini boshqardik. Bugun ikki "eshik" ni quramiz: foydalanuvchi emaili haqiqatan uniki ekanini tasdiqlash va parolni unutgan odamni qaytarish. Ikkalasi ham bitta g'oyaga tayanadi — xat orqali yuborilgan bir martalik kalit.

1. Nega bu kerak?

Hozir vazifalar-api da istalgan kishi aziz@example.com bilan ro'yxatdan o'tishi mumkin — haqiqiy Aziz bo'lmasa ham. Bu ikki muammo tug'diradi:

  • Begona emailga hisob ochib qo'yiladi. Keyin haqiqiy egasi kelganda "email band" — va hisobda allaqachon kimningdir paroli turibdi.
  • Parolni unutgan foydalanuvchini qaytarishning yo'li yo'q.

Parol tiklash oqimi — ko'pincha tizimning eng zaif eshigi. Hujumchi parolni buzish o'rniga "parolni unutdim" tugmasini bosadi va oqimdagi xatoni qidiradi: oldindan aytib bo'ladigan token, muddati yo'q havola, boshqa domenga ketadigan xat, "bunday email yo'q" degan javob. Bugun ularning har birini yopamiz.

2. Oqim bitta rasmda

sequenceDiagram
  participant B as Brauzer
  participant A as vazifalar-api
  participant D as Postgres
  participant P as Pochta
  B->>A: POST /parol-tiklash/sorov {email}
  A-->>B: 202 (har doim bir xil)
  A->>D: token hash, 30 daqiqa (fonda)
  A->>P: havola .../parol-tiklash#token=...
  B->>A: POST /parol-tiklash {token, parol}
  A->>D: UPDATE ... RETURNING (bir marta)
  A->>D: yangi hash, hamma sessiyalar bekor
  A-->>B: 204

Ikki joyga qarang. Ikkinchi qator: javob token va xatdan oldin qaytadi — bu vaqt orqali sizishni yopadi. Oxiridan bittasi oldingi qator: parol yangilanishi bilan hamma sessiyalar o'ladi. Email tasdiqlash ham xuddi shunday oqim, faqat muddat 24 soat va oxirida sessiya beriladi.

3. Bir martalik token

3.1 To'rt qoida

Qoida Kanon qiymati Nimadan himoya
kriptografik tasodif 32 bayt (randomBytes) taxmin qilish
bazada faqat hash sha256(token), 32 bayt baza sizishi
muddat tasdiq 24 soat, tiklash 30 daqiqa eski xatlar
bir marta atomik UPDATE … RETURNING qayta ishlatish

Tokenni brauzerda yasab ko'ramiz — xuddi kanon kabi 32 bayt, base64url:

js
const b64url = (baytlar) =>
  btoa(String.fromCharCode(...baytlar))
    .replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");

async function sha256hex(matn) {
  const h = await crypto.subtle.digest(
    "SHA-256", new TextEncoder().encode(matn));
  return Array.from(new Uint8Array(h), (b) =>
    b.toString(16).padStart(2, "0")).join("");
}

// 32 bayt = 256 bit tasodif (CSPRNG)
const token = b64url(crypto.getRandomValues(new Uint8Array(32)));
const hash = await sha256hex(token);

console.log(token.length, /^[A-Za-z0-9_-]{43}$/.test(token));
console.log(hash.length, "belgi hex = 32 bayt bazada");
console.log("bazada token bormi:", hash.includes(token));

Konsolda:

text
43 true
64 belgi hex = 32 bayt bazada
bazada token bormi: false

Token har safar boshqa, shuning uchun uning o'zini emas, faqat uzunligi va shaklini chiqardik. crypto.getRandomValues — kriptografik tasodif manbai (CSPRNG), Math.random emas (crypto 1: tasodif, hash va HMAC).

3.2 Nega bu yerda SHA-256 yetadi

Parolni xavfsiz saqlash darsida "parolni hech qachon SHA-256 bilan saqlamang" degandik. Endi o'zimiz SHA-256 ishlatyapmiz. Ziddiyat yo'q — farq kirish ma'lumotining kuchida:

  • Parolni odam o'ylab topadi. parol123 ni taxmin qilish oson, shuning uchun hash sekin bo'lishi kerak.
  • Token — 256 bit sof tasodif. 2²⁵⁶ variantni sinab chiqishga butun dunyo kompyuterlari ham yetmaydi. Tez hash bu yerda xavfsiz va har so'rovda arzon.

Shu bilan birga hash kerak: baza zaxirasi sizsa, ochiq tokenlar bilan hujumchi 30 daqiqa ichida istalgan parolni o'zgartira olardi. Hash bilan esa u faqat ma'nosiz baytlarni ko'radi. Keyingi darsda 6 xonali SMS kodda bu hisob butunlay o'zgarishini ko'ramiz.

3.3 Bir marta — atomik

"Bir marta" ni ikki qadamda tekshirish xato: avval SELECT (ishlatilmaganmi?), keyin UPDATE. Ikkita parallel so'rov ikkalasi ham SELECT dan o'tadi va token ikki marta ishlaydi. Kanon tekshiruv va belgilashni bitta so'rovda qiladi. Lokal bazada (114-migratsiyasi bilan) sinadik — biz ishga tushirdik, PostgreSQL 17.6:

sql
INSERT INTO bir_martalik_tokenlar
  (foydalanuvchi_id, maqsad, token_hash, tugash_vaqti)
VALUES (1, 'parol_tiklash', sha256('namuna-token'),
        now() + interval '30 minutes')
RETURNING id, maqsad, octet_length(token_hash) AS bayt;

Natija:

text
 id |    maqsad     | bayt
----+---------------+------
  1 | parol_tiklash |   32
(1 row)

INSERT 0 1

Postgres'ning o'z sha256() funksiyasi bilan namuna hash yasadik — haqiqiy kodda hash'ni Node hisoblaydi. Endi tokenni ishlatamiz:

sql
UPDATE bir_martalik_tokenlar SET ishlatilgan_vaqt = now()
WHERE token_hash = sha256('namuna-token')
  AND maqsad = 'parol_tiklash'
  AND ishlatilgan_vaqt IS NULL AND tugash_vaqti > now()
RETURNING foydalanuvchi_id;

Natija:

text
 foydalanuvchi_id
------------------
                1
(1 row)

UPDATE 1

UPDATE 1 — psql'ning buyruq tegi: bitta qator o'zgardi. Aynan shu so'rovni ikkinchi marta:

sql
UPDATE bir_martalik_tokenlar SET ishlatilgan_vaqt = now()
WHERE token_hash = sha256('namuna-token')
  AND maqsad = 'parol_tiklash'
  AND ishlatilgan_vaqt IS NULL AND tugash_vaqti > now()
RETURNING foydalanuvchi_id;

Natija:

text
 foydalanuvchi_id
------------------
(0 rows)

UPDATE 0

Postgres UPDATE paytida qatorni qulflaydi: ikkinchi so'rov birinchisi tugashini kutadi, keyin shartni qayta tekshiradi va ishlatilgan_vaqt IS NULL endi yolg'on (Qulflar: row, table va advisory). Kanon testi buni HTTP darajasida tasdiqlaydi: bitta havola bilan bir vaqtda 5 so'rov — [204, 400, 400, 400, 400].

maqsad ustuni tokenlarni bir-biridan ajratadi: tasdiqlash havolasi bilan parol tiklab bo'lmaydi. Ruxsat etilmagan maqsadni baza o'zi rad etadi:

sql
\set VERBOSITY terse
INSERT INTO bir_martalik_tokenlar
  (foydalanuvchi_id, maqsad, token_hash, tugash_vaqti)
VALUES (1, 'kirish', sha256('boshqa'), now() + interval '1 hour');

Natija:

text
ERROR:  new row for relation "bir_martalik_tokenlar" violates check constraint "bir_martalik_tokenlar_maqsad_check"

Tekshirib ko'ring: Foydalanuvchi "parolni unutdim" ni uch marta bosdi va uchta xat oldi. Qaysi havola ishlaydi?

Javob

Faqat oxirgisi. Kanon yangi token yaratganda shu maqsaddagi eski ishlatilmagan tokenlarni bekor qiladi. Sabab: pochta qutisida bir nechta tirik kalit yotmasin. Bundan tashqari, 60 soniya ichidagi qayta bosishlar yangi xat ham yubormaydi — «Qayta yuborish oralig'i» bo'limida.

4. Havola: qayerga va qanday

4.1 Token # dan keyin

Kanon havolasi: https://<ILOVA_URL>/parol-tiklash#token=…. Nega ?token= emas?

js
const token = "Qw3rTy9-namuna-token-faqat-dars-uchun-xxxxx";
const havola = new URL(
  `https://vazifalar.uz/parol-tiklash#token=${token}`);

// Serverga boradigan so'rov qatori: yo'l + query, fragment YO'Q
console.log("GET", havola.pathname + havola.search);
// Brauzerda JS o'qiy oladigan qism
console.log(havola.hash.slice(0, 13) + "…");

const eski = new URL(
  `https://vazifalar.uz/parol-tiklash?token=${token}`);
console.log("GET", (eski.pathname + eski.search).slice(0, 34) + "…");

Konsolda:

text
GET /parol-tiklash
#token=Qw3rTy…
GET /parol-tiklash?token=Qw3rTy9-namun…

# dan keyingi qism — fragment. Brauzer uni serverga yubormaydi va Referer sarlavhasiga ham qo'shmaydi. Natijada token:

  • frontend serverining (nginx, Vite) access log'iga tushmaydi;
  • sahifadagi tashqi rasm yoki analitika skripti so'roviga Referer orqali sizmaydi;
  • proxy va CDN loglarida ko'rinmaydi.

?token= da esa uchala yo'l ochiq. Frontend sahifasi tokenni location.hash dan o'qiydi va API'ga tanada yuboradi: POST /v1/auth/parol-tiklash {token, parol}. Qo'shimcha himoya — sahifaga Referrer-Policy sarlavhasi (Helmet va xavfsizlik header'lari).

4.2 Host header injection

Havoladagi domenni qayerdan olish kerak? Sodda kod so'rovning Host sarlavhasidan yasaydi — "qaysi domenda ishlasak, o'sha". Lekin Host ni so'rov yuboruvchi yozadi:

js
const ILOVA_URL = "https://vazifalar.uz"; // config'dan, .env

// Xato: havola so'rovning Host sarlavhasidan yasaladi
function zaifHavola(sarlavhalar, token) {
  return `https://${sarlavhalar.host}/parol-tiklash#token=${token}`;
}
// To'g'ri: havola faqat sozlamadan
function togriHavola(token) {
  return `${ILOVA_URL}/parol-tiklash#token=${token}`;
}

const hujum = { host: "yovuz.uz" }; // hujumchi o'zgartirgan Host
console.log(zaifHavola(hujum, "T0KEN"));
console.log(togriHavola("T0KEN"));

Konsolda:

text
https://yovuz.uz/parol-tiklash#token=T0KEN
https://vazifalar.uz/parol-tiklash#token=T0KEN

Hujum shunday: hujumchi Azizning emaili bilan tiklash so'raydi va Host: yovuz.uz qo'yadi. Aziz haqiqiy vazifalar dan, haqiqiy xatni oladi — faqat havola hujumchi saytiga olib boradi. Aziz bosadi va token hujumchiga ketadi. Kanon havolani faqat ILOVA_URL sozlamasidan yasaydi (config'da URL sifatida tekshiriladi, sukut http://localhost:5173).

5. Email bor-yo'qligini oshkor qilmaslik

5.1 Bir xil matn

Ro'yxat va login oqimi hamda hisob xavfsizligi darsida login uchun "email yoki parol noto'g'ri" degan bir xil javobni yozgandik. Bu user enumeration (foydalanuvchilarni sanab chiqish) hujumiga qarshi edi. Tiklash so'rovi ham xuddi shu hujum uchun qulay eshik: "bunday email topilmadi" desangiz, hujumchi ro'yxatingizni bilib oladi.

Kanon tiklash so'roviga har doim 202 va bitta matn qaytaradi. Kanon tekshiruvidan — mavjud va noma'lum email (qisqartirilgan, ikki javob ham bir xil Content-Length: 86 va bir xil ETag bilan):

text
$ curl -X POST … -d {"email":"aziz@example.com"} …/v1/auth/parol-tiklash/sorov
HTTP/1.1 202 Accepted
{"xabar":"Agar bu email to'g'ri bo'lsa, unga xat yuborildi. Pochtangizni tekshiring."}

$ curl -X POST … -d {"email":"yoq@example.com"} …/v1/auth/parol-tiklash/sorov
HTTP/1.1 202 Accepted
{"xabar":"Agar bu email to'g'ri bo'lsa, unga xat yuborildi. Pochtangizni tekshiring."}

Ro'yxatdan o'tish ham shunday o'zgardi. Oldingi darslarda band email 409 olardi — bu ham enumeration edi. Endi:

  • yangi email — tasdiqlanmagan hisob va tasdiqlash xati, javob 202, sessiya yo'q;
  • band email — hisob o'zgarmaydi, egasiga "kimdir shu email bilan ro'yxatdan o'tmoqchi bo'ldi" degan xat, javob o'sha 202.

Ikkala holatda ham argon2 bajariladi — hash INSERT dan oldin hisoblanadi.

5.2 Bir xil vaqt — kanon topilmasi

Matn bir xil. Lekin kanon javob vaqtini o'lchadi (navbatma-navbat 60 juft so'rov):

Parol tiklash so'rovi: javob vaqti (mediana)
  • oldin: mavjud emailtranzaksiya + token + xat30 ms
  • oldin: noma'lum emailfaqat jurnal12,8 ms
  • keyin: mavjud emailxat fonda16,6 ms
  • keyin: noma'lum emailfaqat jurnal17,6 ms

Manba: Kanon o'lchovi (IlmHamroh, 2026-10): Node 24.21.0, PostgreSQL 18.6, i5-12500H, 2026-10-27

Birinchi ikki ustun: mavjud email uchun server tranzaksiya ochadi, token yozadi, xat yuboradi — 30.0 ms. Noma'lum email uchun faqat jurnalga bir qator — 12.8 ms. Matni bir xil javob vaqti bilan "bu email bor" deb aytib turgan edi. Bu timing attack (vaqt hujumi).

Tuzatish: token va xat — fon ishi. Javob ikkala yo'lda ham darhol qaytadi; keyingi ikki ustun — 16.6 va 17.6 ms. Ro'yxatdan o'tishda farq kichik edi (167 va 188 ms), chunki ikkala yo'lda ham eng og'ir ish — argon2 — bajariladi.

Bu o'lchov bitta mashinada, band tizimda olingan. Haqiqiy tarmoqda shovqin ko'proq, lekin hujumchi minglab so'rov yuborib o'rtachani oladi — 17 ms farq yaxshi ko'rinadi.

5.3 FonIshlar

Fon ishi uchun kanon kichik sinf yozdi. Uning ikki talabi bor: javob kutmasin va xato jim yutilmasin:

ts
type XatoLogi = {
  error: (obj: { err: unknown; ish: string }, xabar: string) => void;
};

export class FonIshlar {
  readonly #ishlar = new Set<Promise<void>>();
  readonly #log: () => XatoLogi;

  constructor(log: () => XatoLogi) {
    this.#log = log;
  }

  /** Ishni boshlaydi va KUTMAYDI. Xato yutilmaydi — log'ga */
  qosh(nom: string, ish: () => Promise<void>): void {
    const p: Promise<void> = ish()
      .catch((x: unknown) => {
        const xabar = "fon ishi bajarilmadi";
        this.#log().error({ err: x, ish: nom }, xabar);
      })
      .finally(() => {
        this.#ishlar.delete(p);
      });
    this.#ishlar.add(p);
  }

  /** Hamma boshlangan ishlar tugashini kutadi (server to'xtashida va
   * testlarda) */
  async tugashi(): Promise<void> {
    while (this.#ishlar.size > 0) {
      await Promise.all([...this.#ishlar]);
    }
  }
}

tugashi() ikki joyda kerak: server to'xtayotganda (yarim yo'lda qolgan xat yo'qolmasin, Process darajasidagi xatolar va crash strategiyasi) va testlarda (xat kelishini kutish uchun). while — ish ichida yangi ish qo'shilsa, uni ham kutadi.

Halol chegarasi: bu xotiradagi ro'yxat. Jarayon qulasa, boshlangan xat yo'qoladi; pochta serveri javob bermasa — qayta urinish yo'q. Haqiqiy navbat — Nega navbat kerak darsidagi BullMQ kabi vositalar.

6. Tasdiqlanmagan hisob va pre-hijacking

Email tasdiqlanmagan hisob bilan kirishga urinilsa, kanon 403 email-tasdiqlanmagan beradi — lekin faqat parol to'g'ri bo'lsa. Parol noto'g'ri bo'lsa — oddiy "email yoki parol noto'g'ri". Shuning uchun 403 hech narsani oshkor qilmaydi: uni faqat parolni biladigan odam ko'radi.

Nega tasdiqlanmagan hisobga kirishni umuman yopamiz? Pre-hijacking (hisobni oldindan egallash) hujumi uchun:

  1. Hujumchi qurbon@example.com bilan o'z paroli bilan hisob ochadi.
  2. Haqiqiy egasi keyinroq keladi — masalan, "Google bilan kirish" orqali — va tizim uni o'sha hisobga bog'laydi.
  3. Hujumchi o'z paroli bilan qurbonning hisobiga kirib yuradi.

Bu hujumni 2022-yilda Avinash Sudhodanan va Andrew Paverd tadqiq qilgan: ular 75 ta mashhur xizmatni tekshirib, kamida 35 tasida pre-hijacking zaifligini topgan. Ularning asosiy tavsiyasi — emailni tasdiqlamaguncha hisobdan foydalanishga ruxsat bermaslik.

Tasdiqlanmagan hisob bilan kirib bo'lmasa, birinchi qadam hech narsa bermaydi. Qurbon esa parolni tiklash orqali hisobni o'ziga oladi — tiklash emailni ham tasdiqlaydi, hujumchining paroli o'chadi. Kanon testi aynan shu ssenariyni o'ynaydi. Bu himoya Google bilan kirish darsida tashqi hisoblarni bog'laganda ayniqsa muhim.

7. Parol tiklash: uch nozik joy

7.1 Zaif parolda token "yonmasin"

Foydalanuvchi havolani ochib, yangi parol sifatida Aziz2026 ni yozdi. Parol siyosati uni rad etadi (Ro'yxat va login oqimi hamda hisob xavfsizligi) — ism parol ichida. Agar server tokenni avval ishlatib, keyin parolni tekshirsa, havola o'ladi va foydalanuvchi yangi xat so'rashga majbur. Kanon tartibi:

  1. egasi(token) — token kimniki (ishlatmasdan);
  2. parol siyosati — o'sha foydalanuvchining emaili va ismi bilan;
  3. faqat keyin ishlat(token) — atomik.

7.2 Hamma sessiyalar bekor

Odam parolni nega tiklaydi? Ko'pincha unutgani uchun. Lekin ba'zan — hisobi buzilgani uchun. Ikkinchi holatda hujumchining sessiyasi tirik qolsa, yangi parol hech narsani hal qilmaydi. Shuning uchun kanon parol yangilangach hammasiniBekorQil(fid) chaqiradi — hamma qurilmalar, jumladan hujumchiniki. Joriy brauzerning cookie'si ham o'chiriladi va foydalanuvchi yangi parol bilan kiradi.

7.3 Tiklash emailni tasdiqlaydi

Havolani ochgan odam pochtaga egalik qilishini isbotladi. Shuning uchun tiklash email_tasdiqlangan_vaqt ni ham to'ldiradi. Shu tufayli paroli yo'q hisob (masalan, faqat SMS orqali kiradigan) ham shu oqim bilan parol o'rnata oladi.

8. Qayta yuborish oralig'i

"Xatni qayta yuborish" tugmasi ham hujum vositasi: kimdir sizning saytingiz orqali begona pochta qutisini xatlar bilan to'ldirishi va sizning pochta byudjetingizni yeyishi mumkin. Kanon qoidasi: bir foydalanuvchiga, bir maqsad bo'yicha 60 soniyada bitta xat.

Bu tekshiruv ham poygaga uchraydi: ikki parallel so'rov ikkalasi ham "oxirgi xat 60 s dan eski" ni ko'radi. Shuning uchun yarat() tranzaksiya ichida foydalanuvchi qatorini FOR UPDATE bilan qulflaydi — ikkinchi so'rov birinchisi tugashini kutadi va endi yangi tokenni ko'radi. Umumiy so'rovlar sonini cheklash — Rate limiting darsida.

9. Kanonda yo'q, lekin bilish kerak

Magic link — parolsiz kirish: emailga havola keladi, bosasiz — kirdingiz. Mexanizm bugungi tokenning o'zi: 32 bayt, hash, qisqa muddat (odatda 10–15 daqiqa), bir marta. Farqi — havola sessiya beradi, shuning uchun u parolning o'rnini bosadi va pochta qutisi yagona himoyaga aylanadi. Kanon uni qo'shmadi, keyingi darsda esa shunga o'xshash mexanizmni SMS kod bilan quramiz.

9.2 Emailni o'zgartirish

vazifalar-api da email o'zgartirish yo'q. Qo'shilganda OWASP tavsiya qiladigan tartib:

  1. Avval qayta tasdiqlash: joriy parol (yoki ikkinchi faktor).
  2. Yangi manzil alohida "kutilmoqda" holatida saqlanadi va unga tasdiqlash havolasi ketadi — u bosilmaguncha email o'zgarmaydi.
  3. Eski manzilga xat: ikkinchi faktor bo'lsa — ogohlantirish, bo'lmasa — u ham tasdiqlashi kerak. Ikkala xatda "bu men emasman" havolasi bor.
  4. Havolalar bir martalik va muddatli; o'zgarishdan keyin sessiyalar yopiladi.

Aks holda sessiyani bir lahza qo'lga olgan hujumchi emailni o'zgartirib, keyin parolni "qonuniy" tiklab, hisobni butunlay olib ketadi.

9.3 Mavjud hisoblar

114-migratsiyagacha ro'yxatdan o'tganlar emailini tasdiqlamagan — bunday oqim yo'q edi. Ularni "tasdiqlanmagan" qilsak, hech kim kira olmay qoladi. Kanon qarori: mavjud hisoblar tasdiqlangan deb belgilanadi (email_tasdiqlangan_vaqt = yaratilgan_vaqt). Xavfi ham bor: ular orasida begona emailga ochilgan hisob bo'lsa, u ham "tasdiqlangan" bo'lib qoldi. Kursdagi kichik bazada bu ongli kelishuv. Katta tizimda muqobil — eskilarni keyingi kirishda tasdiqlashga majburlash.

10. Hujumchi nigohi

Oxirgi ustun — himoyani tasdiqlagan sinov (email.test.ts yoki darsdagi misol).

Hujum Nima uchun ishlaydi Himoya Test
tokenni taxmin qilish Math.random, qisqa token 32 bayt CSPRNG shakl: 43 belgi
sizgan bazadan tokenlar bilan tiklash token ochiq saqlangan faqat sha256 bazada token emas, sha256
eski xatdagi havola muddat yo'q, eskisi tirik 30 daqiqa, yangisi eskini bekor qiladi qayta yuborish: eski havola bekor
bitta havolani parallel ishlatish SELECT + UPDATE atomik UPDATE … RETURNING 5 so'rov — bitta 204
Host: yovuz.uz bilan tiklash so'rovi havola Host dan yasaladi faqat ILOVA_URL zaifHavola / togriHavola
Referer va log'dan token ?token= #token= URL misoli
"bunday email yo'q" / 409 javob farqli bir xil 202 bir xil 202 tanasi
javob vaqtini o'lchash xat sinxron yuboriladi token va xat fonda kanon o'lchovi: 16.6 / 17.6 ms
oldindan hisob ochish (pre-hijacking) tasdiqlanmagan hisob bilan kirish 403, tiklash egasiga beradi pre-hijacking testi
tiklashdan keyin hujumchi qoladi sessiyalar tirik hammasi bekor BARCHA sessiyalar bekor
"qayta yuborish" bilan spam cheklov yo'q 60 s oraliq, FOR UPDATE 60 s ichida yangi xat yo'q

11. Ko'p uchraydigan xatolar

Xato Nega xavfli To'g'risi
tokenni bazada ochiq saqlash zaxira sizsa — hamma hisob ochiq hash
tekshirish va belgilash alohida ikki marta ishlaydi bitta UPDATE … RETURNING
havolani req.headers.host dan yasash host header injection config ILOVA_URL
"email topilmadi" javobi enumeration bir xil 202
xatni javobdan oldin kutish vaqt farqi fon ishi
fon ishida catch(() => {}) xato jim yo'qoladi log'ga yozish
zaif parolda tokenni ishlatib qo'yish foydalanuvchi qaytadan boshlaydi avval egasi, keyin ishlat
tiklashdan keyin sessiyalarni qoldirish hujumchi ichkarida qoladi hammasiniBekorQil

12. Mashqlar

Mashqlar kurs/mashqlar/28/12-email-tiklash/ da.

1-mashq (oson): to'g'ri yoki noto'g'ri

  1. Tiklash tokeni tasodifiy bo'lgani uchun uni bazada ochiq saqlash mumkin.
  2. Havoladagi #token=… qismi serverning access log'iga tushmaydi.
  3. Email topilmasa 404 qaytarish to'g'ri — REST bo'yicha resurs yo'q.
  4. Parol tiklangandan keyin foydalanuvchining hamma sessiyalari bekor qilinishi kerak.
  5. Tasdiqlanmagan hisob bilan kirishda 403 qaytarish email borligini hamma uchun oshkor qiladi.
Yechim
  1. Noto'g'ri — baza sizsa, ochiq token bilan darhol parol o'zgartiriladi; hash kerak.
  2. To'g'ri — fragment serverga yuborilmaydi.
  3. Noto'g'ri — bu enumeration; javob har doim bir xil 202.
  4. To'g'ri — tiklash sababi hisob buzilishi bo'lishi mumkin.
  5. Noto'g'ri — 403 faqat parol to'g'ri bo'lganda chiqadi; boshqalar oddiy 401 ni ko'radi.

2-mashq (o'rta): token holati

tokenHolati(qator, maqsad, hozir) funksiyasini yozing: "yaroqli", "ishlatilgan", "muddati o'tgan" yoki "topilmadi" qaytarsin. Qator: { maqsad, yaratilgan, ishlatilgan } (ms; ishlatilgan — null yoki vaqt). Muddatlar kanondagidek: tasdiq 24 soat, tiklash 30 daqiqa. Boshqa maqsadning tokeni va yo'q qator — "topilmadi" (sababini oshkor qilmaymiz). Oltita holatni sinang: tiklash 10 va 31 daqiqada; tiklash tokeni bilan tasdiqlash; ishlatilgan; undefined; tasdiqlash 23 soatda. Ishora: obyekt kalitini o'zgaruvchi bilan o'qish — MUDDAT[q.maqsad] (Obyekt: kalit-qiymat juftliklari).

Yechim
js
const MUDDAT = {
  email_tasdiq: 24 * 3600_000,
  parol_tiklash: 30 * 60_000,
};

function tokenHolati(q, maqsad, hozir) {
  if (q === undefined) return "topilmadi";
  if (q.maqsad !== maqsad) return "topilmadi";
  if (q.ishlatilgan !== null) return "ishlatilgan";
  const tugash = q.yaratilgan + MUDDAT[q.maqsad];
  if (hozir >= tugash) return "muddati o'tgan";
  return "yaroqli";
}

const D = 60_000; // daqiqa
const q = {
  maqsad: "parol_tiklash", yaratilgan: 0, ishlatilgan: null,
};
console.log(tokenHolati(q, "parol_tiklash", 10 * D));
console.log(tokenHolati(q, "parol_tiklash", 31 * D));
console.log(tokenHolati(q, "email_tasdiq", 10 * D));
console.log(tokenHolati({ ...q, ishlatilgan: 5 * D },
  "parol_tiklash", 10 * D));
console.log(tokenHolati(undefined, "parol_tiklash", 0));
const t = {
  maqsad: "email_tasdiq", yaratilgan: 0, ishlatilgan: null,
};
console.log(tokenHolati(t, "email_tasdiq", 23 * 60 * D));

Konsolda:

text
yaroqli
muddati o'tgan
topilmadi
ishlatilgan
topilmadi
yaroqli

Uchinchi qator muhim: tiklash tokeni tasdiqlash yo'liga kelsa — "topilmadi", "boshqa maqsad" emas. Foydalanuvchi uchun ham, hujumchi uchun ham javob bir xil: "Havola yaroqsiz". Kanonda ham to'rt holatning hammasi bitta 400 token-yaroqsiz ga aylanadi.

3-mashq (qiyin): xotiradagi BirMartalikTokenlar

Bazasiz kichik model yozing: yarat(fid) va ishlat(token). Qoidalar kanondagidek: 60 soniya ichida qayta so'rov — undefined; yangi token eski ishlatilmaganlarni bekor qiladi; "baza" (massiv) faqat hash saqlaydi (SHA-256, base64url); ishlat — bir marta, foydalanuvchi id qaytaradi. Vaqt — soxta soat (let hozir = 0, qo'lda suriladi). Sinang: birinchi xat; 30 s dan keyin; 61 s dan keyin; eski havola; yangi havola; yana bir marta; bazada ochiq token bormi. Ishora: crypto.subtle.digest, crypto.getRandomValues (crypto 1: tasodif, hash va HMAC), find, some, at(-1).

Yechim
js
const QAYTA = 60_000;
const b64url = (b) => btoa(String.fromCharCode(...b))
  .replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
async function hash(token) {
  const h = await crypto.subtle.digest(
    "SHA-256", new TextEncoder().encode(token));
  return b64url(new Uint8Array(h));
}

let hozir = 0; // soxta soat — testdagi kabi
const qatorlar = []; // "baza": { fid, hash, yaratilgan, ishlatilgan }

async function yarat(fid) {
  const oxirgi = qatorlar.filter((q) => q.fid === fid).at(-1);
  if (oxirgi && hozir - oxirgi.yaratilgan < QAYTA) return undefined;
  for (const q of qatorlar) {
    if (q.fid === fid && q.ishlatilgan === null) {
      q.ishlatilgan = hozir;
    }
  }
  const token = b64url(crypto.getRandomValues(new Uint8Array(32)));
  qatorlar.push({ fid, hash: await hash(token), yaratilgan: hozir,
    ishlatilgan: null });
  return token;
}

async function ishlat(token) {
  const h = await hash(token);
  const q = qatorlar.find(
    (x) => x.hash === h && x.ishlatilgan === null);
  if (q === undefined) return undefined;
  q.ishlatilgan = hozir;
  return q.fid;
}

const birinchi = await yarat(7);
console.log("1-xat:", typeof birinchi);
hozir += 30_000;
console.log("30 s dan keyin:", await yarat(7));
hozir += 31_000;
const ikkinchi = await yarat(7);
console.log("61 s dan keyin:", typeof ikkinchi);
console.log("eski havola:", await ishlat(birinchi));
console.log("yangi havola:", await ishlat(ikkinchi));
console.log("yana bir marta:", await ishlat(ikkinchi));
console.log("bazada ochiq token:",
  qatorlar.some((q) => q.hash === ikkinchi));

Konsolda:

text
1-xat: string
30 s dan keyin: undefined
61 s dan keyin: string
eski havola: undefined
yangi havola: 7
yana bir marta: undefined
bazada ochiq token: false

JavaScript bir oqimli, shuning uchun bu modelda "parallel" muammo ko'rinmaydi: ishlat ichida await dan keyingi qism bitta mikrovazifada bajariladi. Haqiqiy serverda esa so'rovlar turli ulanishlarda keladi va bir nechta server bo'lishi mumkin — u yerda atomiklikni faqat baza kafolatlaydi.

13. Loyiha qadami: tokenlar, xatlar va to'rt yo'l

13.1 Migratsiya sql/114-email-tasdiq-tokenlar.sql

Izohlarni qisqartirdik, SQL kanondagidek:

sql
ALTER TABLE foydalanuvchilar
  ADD COLUMN email_tasdiqlangan_vaqt timestamptz;
-- Mavjud hisoblar — tasdiqlangan (aks holda kira olmay qoladi)
UPDATE foydalanuvchilar
SET email_tasdiqlangan_vaqt = yaratilgan_vaqt
WHERE email_tasdiqlangan_vaqt IS NULL;

CREATE TABLE bir_martalik_tokenlar (
  id               bigint      GENERATED ALWAYS AS IDENTITY,
  foydalanuvchi_id bigint      NOT NULL,
  maqsad           text        NOT NULL,
  token_hash       bytea       NOT NULL,
  yaratilgan_vaqt  timestamptz NOT NULL DEFAULT now(),
  tugash_vaqti     timestamptz NOT NULL,
  ishlatilgan_vaqt timestamptz,
  ip               inet,
  CONSTRAINT bir_martalik_tokenlar_pkey PRIMARY KEY (id),
  CONSTRAINT bir_martalik_tokenlar_foydalanuvchi_id_fkey
    FOREIGN KEY (foydalanuvchi_id)
    REFERENCES foydalanuvchilar (id) ON DELETE CASCADE,
  CONSTRAINT bir_martalik_tokenlar_token_hash_key
    UNIQUE (token_hash),
  CONSTRAINT bir_martalik_tokenlar_maqsad_check
    CHECK (maqsad IN ('email_tasdiq', 'parol_tiklash')),
  CONSTRAINT bir_martalik_tokenlar_token_hash_check
    CHECK (octet_length(token_hash) = 32),
  CONSTRAINT bir_martalik_tokenlar_muddat_check
    CHECK (tugash_vaqti > yaratilgan_vaqt)
);
-- qayta yuborish oralig'i va eskilarini bekor qilish uchun
CREATE INDEX bir_martalik_tokenlar_foydalanuvchi_idx
  ON bir_martalik_tokenlar
  (foydalanuvchi_id, maqsad, yaratilgan_vaqt DESC);

Prisma tomoni: prisma/migrations/20261027140000_email_tasdiq_tokenlar/migration.sql, schema.prisma da BirMartalikToken modeli va Foydalanuvchi.emailTasdiqlanganVaqt. Lokal namuna bazasida backfill natijasi:

sql
SELECT count(*) AS hammasi,
       count(email_tasdiqlangan_vaqt) AS tasdiqlangan
FROM foydalanuvchilar;

Natija:

text
 hammasi | tasdiqlangan
---------+--------------
       8 |            8
(1 row)

13.2 src/auth/token.ts

Telefon uchun bo'lingan, izohlar qisqartirilgan, kod ma'nosi kanondagidek (tip, lint va testlar o'tdi — biz tekshirdik):

ts
// token.ts — bir martalik tokenlar (email tasdiqlash, parol tiklash).
// Token — 32 bayt CSPRNG, bazada sha256, muddatli, bir marta.
import { randomBytes } from "node:crypto";
import type { Pool, QueryResultRow } from "pg";
import { TURLAR } from "../vazifalar/pg-ombor.ts";
import { tokenHash } from "./sessiya.ts";

export type Maqsad = "email_tasdiq" | "parol_tiklash";

/** tasdiqlash xati — 24 soat, parol tiklash — 30 daqiqa */
export const MUDDAT_MS: Record<Maqsad, number> = {
  email_tasdiq: 24 * 3600_000,
  parol_tiklash: 30 * 60_000,
};
/** Qayta yuborish oralig'i: shu vaqt ichida yangi xat yuborilmaydi */
export const QAYTA_YUBORISH_MS = 60_000;

const TOKEN = /^[A-Za-z0-9_-]{43}$/;

export class BirMartalikTokenlar {
  readonly #pool: Pool;
  constructor(pool: Pool) {
    this.#pool = pool;
  }

  async #sorov<T extends QueryResultRow>(
    text: string,
    values: unknown[],
  ): Promise<T[]> {
    const so = { text, values, types: TURLAR };
    return (await this.#pool.query<T>(so)).rows;
  }

  /** Yangi token; eskilari bekor. undefined — oxirgi xat
   * QAYTA_YUBORISH_MS ichida yuborilgan */
  async yarat(
    foydalanuvchiId: number,
    maqsad: Maqsad,
    ip?: string,
  ): Promise<string | undefined> {
    const c = await this.#pool.connect();
    try {
      await c.query("BEGIN");
      // Foydalanuvchi qatori qulfi — parallel ikki so'rov ikkita xat
      // yubormasin
      await c.query(
        "SELECT 1 FROM foydalanuvchilar WHERE id = $1 FOR UPDATE",
        [foydalanuvchiId],
      );
      const { rows } = await c.query<{ yaqinda: boolean }>(
        `SELECT max(yaratilgan_vaqt)
           > now() - $3 * interval '1 millisecond' AS yaqinda
         FROM bir_martalik_tokenlar
         WHERE foydalanuvchi_id = $1 AND maqsad = $2`,
        [foydalanuvchiId, maqsad, QAYTA_YUBORISH_MS],
      );
      if (rows[0]?.yaqinda === true) {
        await c.query("ROLLBACK");
        return undefined;
      }
      await c.query(
        `UPDATE bir_martalik_tokenlar SET ishlatilgan_vaqt = now()
         WHERE foydalanuvchi_id = $1 AND maqsad = $2
           AND ishlatilgan_vaqt IS NULL`,
        [foydalanuvchiId, maqsad],
      );
      const token = randomBytes(32).toString("base64url");
      await c.query(
        `INSERT INTO bir_martalik_tokenlar
           (foydalanuvchi_id, maqsad, token_hash, tugash_vaqti, ip)
         VALUES ($1, $2, $3,
           now() + $4 * interval '1 millisecond', $5)`,
        [
          foydalanuvchiId,
          maqsad,
          tokenHash(token),
          MUDDAT_MS[maqsad],
          ip ?? null,
        ],
      );
      await c.query("COMMIT");
      return token;
    } catch (x) {
      await c.query("ROLLBACK");
      throw x;
    } finally {
      c.release();
    }
  }

  /** Ishlatmasdan: token kimniki (parol siyosati uchun) */
  async egasi(
    token: string,
    maqsad: Maqsad,
  ): Promise<number | undefined> {
    if (!TOKEN.test(token)) return undefined;
    const [q] = await this.#sorov<{ fid: number }>(
      `SELECT foydalanuvchi_id AS fid FROM bir_martalik_tokenlar
       WHERE token_hash = $1 AND maqsad = $2
         AND ishlatilgan_vaqt IS NULL AND tugash_vaqti > now()`,
      [tokenHash(token), maqsad],
    );
    return q?.fid;
  }

  /** Atomik, bir marta → foydalanuvchi id yoki undefined */
  async ishlat(
    token: string,
    maqsad: Maqsad,
  ): Promise<number | undefined> {
    if (!TOKEN.test(token)) return undefined;
    const [q] = await this.#sorov<{ fid: number }>(
      `UPDATE bir_martalik_tokenlar SET ishlatilgan_vaqt = now()
       WHERE token_hash = $1 AND maqsad = $2
         AND ishlatilgan_vaqt IS NULL AND tugash_vaqti > now()
       RETURNING foydalanuvchi_id AS fid`,
      [tokenHash(token), maqsad],
    );
    return q?.fid;
  }
}

tokenHash — sessiya tokeni uchun yozilgan o'sha sha256 funksiyasi (Session-based auth). TOKEN regex'i noto'g'ri shakldagi qiymatni bazaga yubormaydi.

13.3 Pochta va fon

src/auth/pochta.ts — Pochtachi interfeysi (yubor({ kimga, mavzu, matn })) va ikkita amalga oshirish: XotiraPochtachi (testlar uchun, yuborilgan xatlar ro'yxati) va LogPochtachi (lokal dev: xat matni log'ga yoziladi, havolani terminaldan olasiz). Haqiqiy SMTP ulanmadi — uni Email yuborish: SMTP va Nodemailer darsida ko'rgansiz, vazifalar-api ga keyinroq ulanadi. Ungacha xavfli holat yopiq: NODE_ENV=production bo'lsa config xato beradi — log'ga tushgan tiklash havolasi production'da parol kaliti bo'lardi.

src/umumiy/fon.ts — «FonIshlar» bo'limidagi sinf. Config'ga yangi o'zgaruvchi: ILOVA_URL (sukut http://localhost:5173, faqat http/https URL).

13.4 xizmat.ts: ro'yxat va tiklash

Ro'yxat — har doim bir xil, xatlar fonda (bo'lingan):

ts
    async royxat(r: Royxat, mijoz: Mijoz): Promise<void> {
      const hash = await parolHash(r.parol);
      try {
        const f = await ombor.yarat({
          email: r.email, toliqIsm: r.toliqIsm, parolHash: hash,
        });
        await ombor.hodisa({
          tur: "royxat", foydalanuvchiId: f.id, ...mijoz,
        });
        d.fon.qosh("tasdiq-xati", () => tasdiqXati(f, mijoz));
      } catch (x) {
        if (!(x instanceof EmailBandXatosi)) throw x;
        const egasi = await ombor.emailBilan(r.email);
        await ombor.hodisa({
          tur: "royxat_band", foydalanuvchiId: egasi?.id, ...mijoz,
        });
        if (egasi !== undefined) {
          d.fon.qosh("royxat-band-xati", () => pochtachi.yubor({
            kimga: egasi.email,
            mavzu: "vazifalar: hisobingiz bilan " +
              "ro'yxatdan o'tishga urinish",
            matn: "Kimdir shu email bilan yangi hisob ochmoqchi " +
              "bo'ldi. Hisobingiz o'zgarmadi.\nParolni unutgan " +
              `bo'lsangiz: ${d.ilovaUrl}/parol-tiklash`,
          }));
        }
      }
    },

Parol tiklash — «uch nozik joy» kod ko'rinishida:

ts
    async parolTikla(
      token: string,
      yangiParol: string,
      mijoz: Mijoz,
    ): Promise<number> {
      // Avval (ishlatmasdan) kimniki — parol siyosati shaxsiy
      // ma'lumot bilan; yomon parolda token "yonib ketmasin"
      const egasi =
        await tokenlar.egasi(token, "parol_tiklash" satisfies Maqsad);
      const h =
        egasi === undefined ? undefined : await ombor.idBilan(egasi);
      if (h === undefined) throw new TokenXatosi();
      const sabab = taqiqlanganmi(yangiParol, [h.email, h.toliqIsm]);
      if (sabab !== undefined) {
        throw new ValidatsiyaXatosi([
          { joy: "body.parol", xabar: sabab },
        ]);
      }
      const fid = await tokenlar.ishlat(token, "parol_tiklash");
      if (fid === undefined) throw new TokenXatosi();
      await ombor.parolniYoz(fid, await parolHash(yangiParol));
      await ombor.emailTasdiqla(fid);
      const bekor = await d.sessiyalar.hammasiniBekorQil(fid);
      await ombor.hodisa({
        tur: "parol_tiklandi", foydalanuvchiId: fid, ...mijoz,
        tafsilot: { bekorSessiyalar: bekor },
      });
      return bekor;
    },

tiklashSorovi ham shu uslubda: jurnalga { topildi: true|false } yozadi (javobda emas!), foydalanuvchi topilsa — token va xatni d.fon.qosh(…) ga beradi. Kirishda yangi shart: parol to'g'ri, lekin emailTasdiqlangan === null — 403 email-tasdiqlanmagan.

13.5 Yo'llar

Yo'l Tana Javob
POST /v1/auth/royxat email, ism, parol 202, sessiyasiz
POST /v1/auth/email-tasdiq { token } 200 + sessiya; 400 token-yaroqsiz
POST /v1/auth/email-tasdiq/qayta { email } 202
POST /v1/auth/parol-tiklash/sorov { email } 202
POST /v1/auth/parol-tiklash { token, parol } 204, cookie o'chadi; 422; 400

Diqqat: ro'yxat endi 201 va avtomatik kirish bermaydi. Oldingi testlar va e2e shunga moslandi: yordamchi hisobOch ro'yxat → xat → tasdiqlash qiladi, xatToken esa fon tugashini kutib xatdan tokenni oladi.

13.6 Testlar

test/email.test.ts — 7 ta yangi test, test/fon.test.ts — 1 ta, config.test — 1 ta. Parallel ishlatish testi (bo'lingan):

ts
const token = await xatToken(d);
const n = await Promise.all(Array.from({ length: 5 }, (_, i) =>
  request(app).post("/v1/auth/parol-tiklash")
    .send({ token, parol: `${YANGI}-${String(i)}` })));
assert.deepEqual(n.map((x) => x.status).sort(),
  [204, 400, 400, 400, 400]);

Biz ishga tushirdik — lokal PostgreSQL 17.6, kanon nusxasi va yuqoridagi bo'lingan fayllar bilan, Node 24.21.0:

bash
node --test test/email.test.ts
text
✔ tasdiqlash: tasdiqlanmagan hisob bilan kirish — 403; havola → sessiya; o'sha havola ikkinchi marta — 400 (316.8515ms)
✔ bazada token emas, sha256; noto'g'ri shakl, begona maqsad, muddati o'tgan — 400 (166.4977ms)
✔ qayta yuborish: 60 s ichida yangi xat yo'q; keyin — yangi xat, eski havola bekor (154.5234ms)
✔ parol tiklash so'rovi: mavjud va noma'lum email — bir xil 202 tanasi; xat faqat mavjudga; jurnalda farq (145.87ms)
✔ parol tiklash: zaif parol — 422 va havola YONMAYDI; yaxshi parol — 204, BARCHA sessiyalar bekor, eski parol 401, yangisi 200; havola qayta — 400 (333.6879ms)
✔ bitta havola bilan bir vaqtda 5 so'rov — faqat bittasi muvaffaqiyatli (UPDATE … RETURNING atomik) (266.2958ms)
✔ pre-hijacking: hujumchi qurbonning emailiga oldindan hisob ochadi — kira olmaydi (tasdiqlanmagan); qurbon parolni tiklab hisobni oladi (333.7068ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4142.4928

13.7 Tekshiruv va commit

Kanon tekshiruvi (2026-10-27, PostgreSQL 18.6): tip va lint xatosiz, testlar 157 (156 + 1 o'tkazilgan), e2e 4/4, Prisma migratsiyalari va sql/ bir xil. Jonli tekshiruvda ikkala tiklash so'rovi bir xil 202 qaytardi.

bash
M="feat(auth): email tasdiqlash va parol tiklash — bir martalik"
M+=" tokenlar (hash, muddat, bir marta), enumeration'siz javob,"
M+=" xatlar fonda; db: 114"
git add src test e2e sql prisma
git commit -m "$M"

14. Real ishda

  • Har katta xizmatda shu oqim bor: "Parolni unutdingizmi?" — bir xil javob, qisqa muddatli havola, keyin "boshqa qurilmalardan chiqildi" degan xat.
  • Xatlar spam'ga tushmasligi — alohida san'at: SPF, DKIM, DMARC (Email yetkazib berish). Tiklash xati kelmasa, foydalanuvchi uchun bu "sayt ishlamaydi" degani.
  • Freymvorklar va auth xizmatlari bu oqimni tayyor beradi (Auth kutubxonalari va xizmatlari). Ularni tanlaganda shu darsdagi jadval bilan tekshiring: token hash'lanadimi, muddat qancha, sessiyalar bekor bo'ladimi.
  • Intervyuda: "Parol tiklashni qanday xavfsiz qilasiz?", "Nega tokenni hash qilamiz, agar u tasodifiy bo'lsa?", "User enumeration nima va uni qanday yopasiz?", "Host header injection nima?".

Xulosa

  • Bir martalik token: 32 bayt CSPRNG, bazada sha256, muddat (24 soat / 30 daqiqa), bir marta — atomik UPDATE … RETURNING.
  • Token havolaning # qismida — server log'i va Referer ga tushmaydi; havola domeni faqat ILOVA_URL dan.
  • Enumeration ikki kanaldan yopiladi: bir xil javob (202) va bir xil vaqt (xat fonda) — kanon 30.0 va 12.8 ms farqni o'lchab topdi.
  • Tasdiqlanmagan hisob bilan kirib bo'lmaydi (pre-hijacking); 403 faqat to'g'ri parolda.
  • Tiklashda avval token egasi va parol siyosati, keyin tokenni ishlatish; oxirida hamma sessiyalar bekor.
  • Qayta yuborish — 60 s oraliq, foydalanuvchi qatori FOR UPDATE bilan qulflanadi.

Keyingi dars: OTP orqali login — telefon raqam va SMS kod bilan kirish: 6 xonali kodda nega SHA-256 yetmaydi, urinishlar chegarasi va SMS pumping firibgarligi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Email tasdiqlash va parol tiklash oqimlari: bir martalik token, muddat va enumeration'siz javob — IlmHamroh