Mundarija (41)
- Bu darsda
- 1. Nega bu kerak?
- 2. Oqim bitta rasmda
- 3. Bir martalik token
- 3.1 To'rt qoida
- 3.2 Nega bu yerda SHA-256 yetadi
- 3.3 Bir marta — atomik
- 4. Havola: qayerga va qanday
- 4.1 Token # dan keyin
- 4.2 Host header injection
- 5. Email bor-yo'qligini oshkor qilmaslik
- 5.1 Bir xil matn
- 5.2 Bir xil vaqt — kanon topilmasi
- 5.3 FonIshlar
- 6. Tasdiqlanmagan hisob va pre-hijacking
- 7. Parol tiklash: uch nozik joy
- 7.1 Zaif parolda token "yonmasin"
- 7.2 Hamma sessiyalar bekor
- 7.3 Tiklash emailni tasdiqlaydi
- 8. Qayta yuborish oralig'i
- 9. Kanonda yo'q, lekin bilish kerak
- 9.1 Magic link
- 9.2 Emailni o'zgartirish
- 9.3 Mavjud hisoblar
- 10. Hujumchi nigohi
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): token holati
- 3-mashq (qiyin): xotiradagi BirMartalikTokenlar
- 13. Loyiha qadami: tokenlar, xatlar va to'rt yo'l
- 13.1 Migratsiya sql/114-email-tasdiq-tokenlar.sql
- 13.2 src/auth/token.ts
- 13.3 Pochta va fon
- 13.4 xizmat.ts: ro'yxat va tiklash
- 13.5 Yo'llar
- 13.6 Testlar
- 13.7 Tekshiruv va commit
- 14. Real ishda
- Xulosa
- Manbalar
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 … RETURNINGbilan. 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 havolaniHostsarlavhasidan 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-apiga 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: 204Ikki 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:
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:
43 true
64 belgi hex = 32 bayt bazada
bazada token bormi: falseToken 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.
parol123ni 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:
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:
id | maqsad | bayt
----+---------------+------
1 | parol_tiklash | 32
(1 row)
INSERT 0 1Postgres'ning o'z sha256() funksiyasi bilan namuna hash yasadik — haqiqiy kodda hash'ni Node hisoblaydi. Endi tokenni ishlatamiz:
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:
foydalanuvchi_id
------------------
1
(1 row)
UPDATE 1UPDATE 1 — psql'ning buyruq tegi: bitta qator o'zgardi. Aynan shu so'rovni ikkinchi marta:
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:
foydalanuvchi_id
------------------
(0 rows)
UPDATE 0Postgres 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:
\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:
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?
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:
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
Refererorqali 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:
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:
https://yovuz.uz/parol-tiklash#token=T0KEN
https://vazifalar.uz/parol-tiklash#token=T0KENHujum 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):
$ 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):
- 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:
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:
- Hujumchi
qurbon@example.combilan o'z paroli bilan hisob ochadi. - Haqiqiy egasi keyinroq keladi — masalan, "Google bilan kirish" orqali — va tizim uni o'sha hisobga bog'laydi.
- 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:
egasi(token)— token kimniki (ishlatmasdan);- parol siyosati — o'sha foydalanuvchining emaili va ismi bilan;
- 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
9.1 Magic link
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:
- Avval qayta tasdiqlash: joriy parol (yoki ikkinchi faktor).
- Yangi manzil alohida "kutilmoqda" holatida saqlanadi va unga tasdiqlash havolasi ketadi — u bosilmaguncha email o'zgarmaydi.
- Eski manzilga xat: ikkinchi faktor bo'lsa — ogohlantirish, bo'lmasa — u ham tasdiqlashi kerak. Ikkala xatda "bu men emasman" havolasi bor.
- 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
- Tiklash tokeni tasodifiy bo'lgani uchun uni bazada ochiq saqlash mumkin.
- Havoladagi
#token=…qismi serverning access log'iga tushmaydi. - Email topilmasa
404qaytarish to'g'ri — REST bo'yicha resurs yo'q. - Parol tiklangandan keyin foydalanuvchining hamma sessiyalari bekor qilinishi kerak.
- Tasdiqlanmagan hisob bilan kirishda
403qaytarish email borligini hamma uchun oshkor qiladi.
Yechim
- Noto'g'ri — baza sizsa, ochiq token bilan darhol parol o'zgartiriladi; hash kerak.
- To'g'ri — fragment serverga yuborilmaydi.
- Noto'g'ri — bu enumeration; javob har doim bir xil 202.
- To'g'ri — tiklash sababi hisob buzilishi bo'lishi mumkin.
- 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
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:
yaroqli
muddati o'tgan
topilmadi
ishlatilgan
topilmadi
yaroqliUchinchi 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
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:
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: falseJavaScript 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:
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:
SELECT count(*) AS hammasi,
count(email_tasdiqlangan_vaqt) AS tasdiqlangan
FROM foydalanuvchilar;Natija:
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):
// 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):
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:
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):
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:
node --test test/email.test.ts✔ 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.492813.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.
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 vaRefererga tushmaydi; havola domeni faqatILOVA_URLdan. - 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 UPDATEbilan 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
- OWASP Forgot Password Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html (2026-10-28 da o'qildi)
- OWASP Authentication Cheat Sheet (bir xil javob): https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- OWASP, "Changing A User's Registered Email Address": https://owasp.org/www-community/pages/controls/Changing_Registered_Email_Address_For_An_Account (2026-10-28 da o'qildi)
- Avinash Sudhodanan, Andrew Paverd, "Pre-hijacked accounts: An Empirical Study of Security Failures in User Account Creation on the Web" — 31st USENIX Security Symposium, 2022: https://www.usenix.org/conference/usenixsecurity22/presentation/sudhodanan
- MDN: "Referer", "URL: hash", "Referrer-Policy" — https://developer.mozilla.org
- Kanon o'lchovi (IlmHamroh, 2026-10-27): parol tiklash javob vaqti (oldin va keyin), holat 12 tekshiruvi. Darsdagi brauzer misollari, SQL (PostgreSQL 17.6),
email.test.tsva bo'lingan fayllar — biz ishga tushirdik (2026-10-28, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!