Mundarija (44)
- Bu darsda
- 1. Nega bu kerak?
- 1.1 Login — eng ko'p uriladigan eshik
- 1.2 Bugungi reja
- 2. Email: bitta odam — bitta hisob
- 2.1 Registr va bo'shliqlar
- 2.2 Ikki qavat himoya
- 3. Parol siyosati: NIST nima deydi
- 3.1 Eski qoidalar nega ketdi
- 3.2 15 belgi — bu qiyin emasmi?
- 4. Taqiqlangan parollar
- 4.1 Lokal ro'yxat va qoidalar
- 4.2 Have I Been Pwned va k-anonymity
- 5. User enumeration: javob matni
- 5.1 Uch xil xato — bitta javob
- 5.2 Ro'yxatda esa hali ochiq
- 6. Timing attack: javob vaqti
- 6.1 Kanon o'lchovi
- 6.2 O'lchash usuli ham muhim
- 7. Brute force va credential stuffing
- 7.1 Lockout tuzog'i
- 7.2 Kanon nima qildi
- 8. Auth jurnali
- 8.1 Nima yoziladi va nima yozilmaydi
- 8.2 O'z bazangizda: hujumni ko'rish
- 9. Ortiqcha maydonlar va chegaralar
- 10. Hujumchi nigohi
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): parol siyosati
- 3-mashq (qiyin): k-anonymity tekshiruvi
- 13. Loyiha qadami: ro'yxat, login va jurnal
- 13.1 Migratsiya
- 13.2 src/auth/sxema.ts
- 13.3 src/auth/taqiqlangan-parollar.ts
- 13.4 src/auth/ombor.ts
- 13.5 src/auth/xizmat.ts
- 13.6 src/auth/marshrut.ts va ulanish
- 13.7 Testlar
- 13.8 Tekshiruv va commit
- 14. Real ishda
- Xulosa
- Manbalar
Ro'yxat va login oqimi hamda hisob xavfsizligi
Qisqacha: Login endpoint'i — hujumchining birinchi nishoni. Uni himoyalashning to'rt qoidasi bor. Javob bir xil: "email yoki parol noto'g'ri", qaysi biri ekanini aytmaymiz. Vaqt bir xil: email bazada bo'lmasa ham argon2 ishlaydi. Har urinish jurnalga yoziladi. Parol siyosati NIST bo'yicha: kamida 15 belgi, "katta harf + raqam" talabi yo'q, keng tarqalgan parollar rad etiladi. Kanon o'lchadi: zaif kodda mavjud email 38.2 ms, noma'lum email 5.2 ms javob beradi — 7 barobar farq email borligini oshkor qiladi.
Bu darsda
- Ro'yxat va login oqimini qadamma-qadam yozasiz: sxema, servis, router, jurnal.
- NIST SP 800-63B-4 parol talablarini kanon tanlovi bilan solishtirasiz.
- User enumeration va timing attack (vaqt hujumi) ni o'lchab ko'rasiz va yopasiz.
- Brute force, credential stuffing, account lockout va tezlik cheklovi farqini tushuntirasiz.
vazifalar-apiga/v1/auth/royxatva/v1/auth/kirishendpoint'larini,auth_hodisalarjadvalini va 10 ta testni qo'shasiz.
Oldin bilishingiz kerak: Parolni xavfsiz saqlash (parolHash, parolTekshir, soxtaHash), Zod 4 serverda, Status kodlarni to'g'ri tanlash, Supertest bilan HTTP integration test.
Oldingi darsda parolni qanday saqlashni o'rgandik, lekin hali hech kim ro'yxatdan o'tmaydi va kirmaydi. Bugun eshik ochiladi: API'da ikkita yangi endpoint paydo bo'ladi. Eshik bor joyda esa uni buzishga urinadiganlar ham bor.
1. Nega bu kerak?
1.1 Login — eng ko'p uriladigan eshik
Saytingizda yuzta endpoint bo'lishi mumkin, lekin hujumchi uchun eng qiziqarlisi bittasi: POST /kirish. Undan o'tsa, qolgan hamma eshik o'zi ochiladi. Shuning uchun avtomatik skanerlar birinchi bo'lib login formasini qidiradi.
Login'ga qarshi to'rt asosiy hujum bor:
| Hujum | Nima qiladi | Misol |
|---|---|---|
| brute force (kuch bilan terish) | bitta hisobga ko'p parol sinaydi | aziz@example.com + 10 000 mashhur parol |
| credential stuffing | boshqa saytdan sizgan juftliklarni sinaydi | 1 million "email:parol" juftligi |
| user enumeration (foydalanuvchilarni sanash) | qaysi email ro'yxatdan o'tganini aniqlaydi | javob matni yoki vaqti farq qiladi |
| password spraying | ko'p hisobga bitta mashhur parol sinaydi | 10 000 email + Toshkent2026 |
Password spraying ayyorroq: har hisobga faqat 1–2 urinish, shuning uchun "bitta hisobga ko'p urinish" degan oddiy himoya uni ko'rmaydi.
1.2 Bugungi reja
Login so'rovining yo'li — ketma-ketlik diagrammasida. Yuqoridan pastga o'qing: kim kimga nima yuboradi.
sequenceDiagram
participant B as Brauzer
participant A as API
participant D as Postgres
B->>A: POST /v1/auth/kirish
A->>A: Zod (xato bo'lsa 422)
A->>D: email bo'yicha hisob
D-->>A: hisob yoki hech narsa
A->>A: argon2 (hash yoki soxtaHash)
A->>D: jurnalga yozish va sabab
alt parol to'g'ri
A-->>B: 200 va profil
else har qanday xato
A-->>B: bir xil 401
endMuhim joy: argon2 har doim ishlaydi, xato javobi esa bitta. Sabab faqat jurnalda — u hujumchiga emas, bizga kerak.
2. Email: bitta odam — bitta hisob
2.1 Registr va bo'shliqlar
Foydalanuvchi bir kuni Aziz@Example.com, ertasiga aziz@example.com deb yozadi. Bu bitta pochta qutisi. Agar baza ularni ikki xil deb bilsa — bitta odamda ikki hisob paydo bo'ladi, yoki u o'z hisobiga kira olmaydi.
const normal = (email) => email.trim().toLowerCase();
const KELGAN = [
"Aziz@Example.com",
" aziz@example.com ",
"AZIZ@EXAMPLE.COM",
];
const noyob = new Set(KELGAN.map(normal));
console.log(noyob.size, [...noyob][0]);
// pochta xizmatining o'z qoidalari — server bilmaydi
console.log(normal("a.ziz+test@gmail.com"));Konsolda:
1 aziz@example.com
a.ziz+test@gmail.comUchta yozuv — bitta email. Ikkinchi qator: ba'zi pochta xizmatlari nuqta va + dan keyingi qismni e'tiborsiz qoldiradi, lekin bu xizmatga xos qoida. Shuning uchun kanon faqat bo'shliq va registrni normallashtiradi.
2.2 Ikki qavat himoya
Kanonda bu qoida ikki joyda turadi:
- Zod sxemasida —
trim(),toLowerCase(), uzunligi ko'pi bilan 254 belgi (RFC 5321 dagi chegara), keyinz.email(). - Bazada — 26-qismdan beri
foydalanuvchilar_email_keynoyob indeksilower(email)ifodasi ustida turadi. Ikki so'rov bir vaqtda bitta emailni ro'yxatdan o'tkazmoqchi bo'lsa ham, baza ikkinchisini23505xatosi bilan rad etadi.
Sxema — tushunarli xato xabari uchun. Indeks — kafolat uchun: tekshiruv va yozuv orasida boshqa so'rov "sirg'alib o'tsa" ham (Read phenomena va izolyatsiya darajalari), baza ikkinchi qatorni qabul qilmaydi. Postgres'da citext turi ham bor; kanon ifoda indeksida qoldi.
3. Parol siyosati: NIST nima deydi
3.1 Eski qoidalar nega ketdi
Ko'p saytlar hali ham shunday talab qiladi: "kamida 8 belgi, bitta katta harf, bitta raqam, bitta maxsus belgi, har 90 kunda almashtiring". Natija hammaga tanish: Parol123!, keyin Parol124!, keyin Parol125!. Qoida bajarildi, parol esa kuchsiz qoldi.
NIST SP 800-63B-4 (2025-yil iyul, «Password Verifiers» bo'limi; pages.nist.gov, 2026-10-28 da o'qildi) bu qoidalardan voz kechdi. Asosiy talablar:
| Talab | NIST SP 800-63B-4 | Kanon (sxema.ts) |
|---|---|---|
| minimal uzunlik | faqat parol bilan kiriladigan hisobda 15; MFA ichida — kamida 8 | 15 (MFA hali yo'q) |
| maksimal uzunlik | kamida 64 gacha ruxsat berish tavsiya etiladi | 128 |
| tarkib qoidalari | taqiqlangan ("katta harf + raqam" kabi qo'shimcha qoidalar qo'yilmaydi) | yo'q |
| taqiqlangan ro'yxat | keng tarqalgan, kutiladigan, sizgan parollar bilan solishtirish | lokal ro'yxat + qoidalar |
| davriy almashtirish | taqiqlangan; faqat buzilganiga dalil bo'lsa | yo'q |
| Unicode | NFC ga normallashtirish tavsiya etiladi | normalize("NFC") |
| parol menejeri, "paste" | ruxsat berilishi shart / tavsiya etiladi | cheklov yo'q |
| kesish (truncation) | butun parol tekshiriladi | argon2 butun parolni oladi |
Kanon tanlovi NIST bilan mos keladi. Bitta nozik joy: 15 belgi talabi faqat bir faktorli hisob uchun. Keyinroq ikki faktorli autentifikatsiya qo'shilganda NIST 8 belgiga ham ruxsat beradi, lekin kanon 15 da qoladi — qo'shimcha xavfsizlik zarar qilmaydi.
3.2 15 belgi — bu qiyin emasmi?
15 ta tasodifiy belgi — qiyin, ibora esa oson: Qovun pishdi, ariq to'ldi — 25 belgi. Kanon xato xabarida shu maslahatni beradi.
Kanon parol uchun ikki xil sxema ishlatadi:
- ro'yxatda — to'liq siyosat: 15…128 belgi, taqiqlangan ro'yxat;
- login'da — siyosat tekshirilmaydi, faqat 1…512 belgi.
Nega login'da tekshirilmaydi? Siyosat ertaga o'zgarishi mumkin (masalan, 12 dan 15 ga oshdi). Eski, 12 belgili parolli foydalanuvchi baribir kira olishi kerak. Login sxemasining vazifasi — faqat serverni himoya qilish: 1 MB lik "parolni" argon2 ga bermaslik.
4. Taqiqlangan parollar
4.1 Lokal ro'yxat va qoidalar
qwertyuiopasdfgh — 16 belgi, uzunlik talabini bajaradi. Lekin u sizib chiqqan har bir ro'yxatda bor va hujumchi uni birinchi minglikda sinaydi. NIST shuning uchun "blocklist" talab qiladi.
Kanon taqiqlangan-parollar.ts faylida ikki xil tekshiruv qiladi: 25 ta mashhur uzun paroldan iborat kichik ro'yxat va bir nechta qoida. Qoidalarning ikkitasini brauzerda sinab ko'ring:
function ketmaKetmi(p) {
let osish = 0;
let kamayish = 0;
for (let i = 1; i < p.length; i++) {
const d = p.charCodeAt(i) - p.charCodeAt(i - 1);
if (d === 1) osish++;
if (d === -1) kamayish++;
}
return osish >= p.length - 2 || kamayish >= p.length - 2;
}
const SINOV = [
"abcdefghijklmnopq",
"98765432109876543",
"aaaaaaaaaaaaaaaaab",
"Bahor-choyxona-2026",
];
for (const p of SINOV) {
const turli = new Set(p.toLowerCase()).size;
console.log(p, "| turli:", turli, "| ketma-ket:", ketmaKetmi(p));
}Konsolda:
abcdefghijklmnopq | turli: 17 | ketma-ket: true
98765432109876543 | turli: 10 | ketma-ket: true
aaaaaaaaaaaaaaaaab | turli: 2 | ketma-ket: false
Bahor-choyxona-2026 | turli: 13 | ketma-ket: falseketmaKetmi— har keyingi belgining kodi oldingisidan 1 ga katta (yoki kichik) bo'lsa.p.length - 2— bitta "sakrash"ga ruxsat:...9, 0, 9...da9 → 0o'tishi ketma-ketlikni buzmasin.new Set(p).size— nechta turli belgi bor. 18 belgiliaaaa…bda atigi 2 ta; kanon 3 va undan kamini rad etadi.Bahor-choyxona-2026— kursdagi namuna parol, ikkala tekshiruvdan o'tadi.
Yana ikki qoida: parolda xizmat nomi (vazifalar, ilmhamroh) va foydalanuvchining o'z ma'lumoti bo'lmasin — dilnoza-rahimova-1990 ni hujumchi birinchi bo'lib sinaydi.
4.2 Have I Been Pwned va k-anonymity
25 ta parol — juda kichik ro'yxat. Haqiqiy loyihalar Have I Been Pwned (HIBP) xizmatining "Pwned Passwords" bazasidan foydalanadi: unda sizib chiqqan yuz millionlab parol bor. Lekin parolni (hatto uning hash'ini) begona xizmatga yuborish yaxshi fikr emas. Yechim — k-anonymity: serverga hash'ning faqat boshi ketadi.
async function sha1Hex(matn) {
const baytlar = new TextEncoder().encode(matn);
const h = await crypto.subtle.digest("SHA-1", baytlar);
return Array.from(new Uint8Array(h), (b) =>
b.toString(16).padStart(2, "0")).join("").toUpperCase();
}
const hash = await sha1Hex("P@ssw0rd");
const boshi = hash.slice(0, 5);
const qolgani = hash.slice(5);
console.log("to'liq SHA-1: ", hash);
console.log("serverga ketadi:", boshi);
console.log("bizda qoladi: ", qolgani);Konsolda:
to'liq SHA-1: 21BD12DC183F740EE76F27B78EB39C8AD972A757
serverga ketadi: 21BD1
bizda qoladi: 2DC183F740EE76F27B78EB39C8AD972A757Xizmat 21BD1 bilan boshlanadigan hamma hash'larning qolgan qismini (va har biri necha marta sizganini) qaytaradi. Biz o'zimizdagi qolgan qismni shu ro'yxatdan qidiramiz. Xizmat qaysi parolni tekshirganimizni bilmaydi — bitta prefiksga yuzlab hash to'g'ri keladi. Bu yerda SHA-1 xavfsizlik uchun emas, faqat qidiruv kaliti sifatida ishlatiladi.
Halol eslatma: HIBP — tashqi xizmat, kursda sinalmagan. Kanonda faqat interfeys g'oyasi bor; 3-mashqda uni soxta javob bilan yozasiz. Ulashda ikki savol bor: xizmat ishlamasa ro'yxat to'xtaydimi (odatda yo'q — lokal ro'yxat bilan davom etadi) va so'rov qancha kutadi (AbortSignal.timeout, Bekor qilish: AbortController).
5. User enumeration: javob matni
5.1 Uch xil xato — bitta javob
Login muvaffaqiyatsiz bo'lishining uch sababi bor: email bazada yo'q, parol noto'g'ri, hisobning paroli umuman yo'q (keyinroq Telegram yoki Google orqali kirganlar). Zaif kod ularni har xil aytadi: "Bunday foydalanuvchi yo'q", "Parol noto'g'ri". Hujumchi uchun bu sovg'a: u 10 000 emaildan qaysi biri saytda borligini bilib oladi, keyin faqat ularga parol teradi.
Kanon uchala holatda bir xil javob beradi. Kanon tekshiruvidagi jonli so'rov (2026-10-27, PostgreSQL 18.6, curl):
$ curl -X POST … -d {"email":"aziz@example.com","parol":"notogri-parol-12345"} …/v1/auth/kirish
HTTP/1.1 401 Unauthorized
Cache-Control: no-store
Content-Type: application/problem+json; charset=utf-8
{"type":"https://vazifalar-api.example/xatolar/kirish-xato","title":"Kirish muvaffaqiyatsiz","status":401,"detail":"Email yoki parol noto'g'ri","instance":"/v1/auth/kirish","kod":"kirish-xato","sorovId":"9d71748a-…"}Javob — Problem Details formatida, detail: "Email yoki parol noto'g'ri". Buyruq va sorovId ni qisqartirdik. Kanon testi uchta holatni yuboradi va tanalarni (sorovId siz) solishtiradi: new Set(tanalar).size === 1.
Cache-Control: no-store ham bejiz emas: auth javoblari proksida ham, brauzerning "orqaga" tugmasi keshida ham qolmasin.
5.2 Ro'yxatda esa hali ochiq
Halol bo'laylik: band email bilan ro'yxatdan o'tishga urinilsa, kanon hozircha 409 email-band qaytaradi. Bu ham enumeration. Kanon buni ataylab qoldirdi: yopish uchun email tasdiqlash kerak ("xat yubordik" degan bir xil javob) — Email tasdiqlash va parol tiklash oqimlari darsida. Hozircha ro'yxat endpoint'ida tezlik cheklovi ham yo'q.
6. Timing attack: javob vaqti
6.1 Kanon o'lchovi
Matn bir xil. Vaqt-chi? Ko'p qo'llanmalardagi kod shunday:
const h = await ombor.emailBilan(email);
if (h?.parolHash == null) {
return res.status(401).json({ xato: "Email yoki parol noto'g'ri" });
}
const n = await parolTekshir(h.parolHash, parol);Email topilmasa — darhol javob. Topilsa — argon2 ishlaydi (~30 ms). Kanon ikkala variantni haqiqiy Postgres va HTTP bilan o'lchadi (60 so'rov, mediana):
- zaif: mavjud email38,2 ms
- zaif: noma'lum email5,2 ms
- kanon: mavjud email65,6 ms
- kanon: noma'lum email64,4 ms
Manba: Kanon o'lchovi (IlmHamroh, 2026-10): Node 24.21.0, PostgreSQL 18.6, supertest, i5-12500H, Windows 11, 2026-10-27
Zaif variantda farq 7 barobar: 38.2 va 5.2 ms. O'nta so'rovning medianasi bilan hujumchi aniq biladi. Matn bir xil, lekin vaqt sirni aytib beradi.
Kanon variantida 65.6 va 64.4 ms — farq shovqin ichida. Sababi oldingi darsdagi soxtaHash(): email topilmasa, server soxta hash bilan argon2 ni baribir bajaradi. Kanon yo'li sekinroq, chunki u jurnalga ham yozadi.
6.2 O'lchash usuli ham muhim
Kanon birinchi urinishda emaillarni ketma-ket o'lchagan: avval 60 ta mavjud, keyin 60 ta noma'lum. Kanon kodida ham "farq" chiqdi: 83.6 va 60.6 ms. Sabab — kod emas: birinchi to'plam paytida kompyuter boshqa ish bilan band edi. Emaillarni navbatma-navbat (a, b, a, b …) o'lchaganda farq yo'qoldi — yuklama ikkalasiga teng tushdi.
Biz ham shunga duch keldik. Kanonning bu darsdagi testi (holat 05) ketma-ket o'lchaydi: 7 ta "bor", keyin 7 ta "yo'q". Biz uni boshqa ishlar bilan band kompyuterda uch marta ishga tushirdik (Node 24.21.0, PostgreSQL 17.6): ikki marta yiqildi, uchinchisida o'tdi.
✖ timing: noma'lum email ham argon2 bajaradi — mediana farqi kichik (≤ 15 ms)
AssertionError [ERR_ASSERTION]: bor 182.2 ms, yo'q 165.3 ms
✖ timing: noma'lum email ham argon2 bajaradi — mediana farqi kichik (≤ 15 ms)
AssertionError [ERR_ASSERTION]: bor 134.6 ms, yo'q 156.6 msE'tibor bering: ikkinchi yiqilishda "yo'q" sekinroq chiqdi. Demak bu kod xatosi emas — o'lchov shovqini. Bunday test flaky (beqaror) deyiladi (Test strategiyasi va flaky testlar). Kanon keyingi holatda testni navbatma-navbat o'lchashga o'tkazdi va chegarani 20 ms ga qo'ydi — buni sessiya darsida ko'rasiz.
Tekshirib ko'ring: Hujumchining tarmog'i shovqinli: javob vaqtiga ±50 ms tasodifiy kechikish qo'shiladi. 33 ms lik farqni baribir sezsa bo'ladimi?
Javob
Ha. Tasodifiy shovqin ko'p o'lchovda o'rtachaga yaqinlashadi, doimiy farq esa qoladi. Hujumchi har emailga 50–100 so'rov yuborib, medianani solishtiradi. Shuning uchun "tarmoq baribir shovqinli" degan bahona ishlamaydi — farqning o'zini yo'qotish kerak.
7. Brute force va credential stuffing
7.1 Lockout tuzog'i
Eng oddiy g'oya: "5 marta xato — hisob 30 daqiqaga bloklanadi" (account lockout). Bu brute force'ni to'xtatadi. Lekin yangi muammo tug'diradi: hujumchi endi sizning hisobingizni bloklay oladi. Aziz email'ini bilgan har kim uni har 30 daqiqada 5 ta noto'g'ri parol bilan tizimdan tashqarida ushlab turadi. Bu — mavjudlikka hujum (CIA dagi "A"), ya'ni DoS.
NIST SP 800-63B-4 (Rate limiting bo'limi) bitta hisobga ketma-ket muvaffaqiyatsiz urinishlarni ko'pi bilan 100 ta bilan cheklashni talab qiladi va bu yuqori chegara ekanini aytadi. Qattiq lockout o'rniga odatda yumshoqroq vositalar ishlatiladi:
| Usul | Nimaga qarshi | Kamchiligi |
|---|---|---|
| hisob va IP bo'yicha tezlik cheklovi (rate limiting) | brute force, bitta IP dan stuffing | botnet minglab IP ishlatadi |
| progressive delay (o'sib boruvchi kechikish) | brute force | serverda kutish holati |
| CAPTCHA (ma'lum urinishdan keyin) | botlar | odam uchun noqulay |
| ikkinchi omil (MFA) | hammasi | foydalanuvchi qadami ko'payadi |
7.2 Kanon nima qildi
Bu darsda kanon tezlik cheklovini hali qo'ymadi. U poydevorni qo'ydi: har urinish auth_hodisalar jadvaliga IP, vaqt va sabab bilan yoziladi. Jadval indekslari aynan shu savollarga mo'ljallangan: "bu hisobga oxirgi 15 daqiqada nechta xato?" (foydalanuvchi_id, vaqt) va "bu IP dan nechta?" (ip, vaqt). Cheklovning o'zi — Tezlik cheklovi darsida, CAPTCHA esa CAPTCHA tokenini serverda tekshirish darsida.
8. Auth jurnali
8.1 Nima yoziladi va nima yozilmaydi
sql/111-auth-hodisalar.sql — OWASP Top 10:2025 dagi A09 (Security Logging and Alerting Failures) ga javob. Har ro'yxat, muvaffaqiyatli va muvaffaqiyatsiz kirish — bitta qator.
Ikki qat'iy qoida:
- Sir hech qachon yozilmaydi: parol, token, kod. Noto'g'ri parol ham — u ko'pincha to'g'ri parolning xato terilgani.
- Noma'lum email yozilmaydi:
foydalanuvchi_idNULL,tafsilotda faqat sababemail-yoq. Begona email — keraksiz shaxsiy ma'lumot.
8.2 O'z bazangizda: hujumni ko'rish
Quyidagi so'rovlar holat 05 sxemasi (namuna ma'lumotlari bilan) qurilgan lokal bazada bajarildi (biz ishga tushirdik, PostgreSQL 17.6). Avval bitta IP dan credential stuffing'ni taqlid qilamiz — 30 ta muvaffaqiyatsiz urinish, har xil hisoblarga:
INSERT INTO auth_hodisalar (foydalanuvchi_id, tur, ip, tafsilot)
SELECT CASE WHEN i % 3 = 0 THEN NULL ELSE i % 3 END,
'kirish_xato', '203.0.113.7',
jsonb_build_object('sabab',
CASE WHEN i % 3 = 0 THEN 'email-yoq' ELSE 'parol' END)
FROM generate_series(1, 30) AS i;Natija:
INSERT 0 30Bitta oddiy foydalanuvchi ham bir marta adashdi:
INSERT INTO auth_hodisalar (foydalanuvchi_id, tur, ip, tafsilot)
VALUES (2, 'kirish_xato', '198.51.100.20', '{"sabab": "parol"}'),
(2, 'kirish', '198.51.100.20', '{}');Natija:
INSERT 0 2Endi "kim hujum qilyapti?" degan savol:
SELECT host(ip) AS ip,
count(*) AS xato,
count(DISTINCT foydalanuvchi_id) AS hisob,
count(*) FILTER (WHERE tafsilot->>'sabab' = 'email-yoq')
AS email_yoq
FROM auth_hodisalar
WHERE tur = 'kirish_xato' AND vaqt > now() - interval '15 minutes'
GROUP BY ip
ORDER BY xato DESC;Natija:
ip | xato | hisob | email_yoq
---------------+------+-------+-----------
203.0.113.7 | 30 | 2 | 10
198.51.100.20 | 1 | 1 | 0
(2 rows)Birinchi IP — aniq hujum: 15 daqiqada 30 ta xato, uchdan biri umuman mavjud bo'lmagan emaillarga. Ikkinchisi — oddiy odam. count(DISTINCT foydalanuvchi_id) NULL'larni sanamaydi, shuning uchun hisob faqat mavjud hisoblarni ko'rsatadi. host(ip) — inet turidan /32 siz manzil.
Jadval jurnalga axlat yozilishiga ham qarshi:
\set VERBOSITY terse
INSERT INTO auth_hodisalar (tur) VALUES ('Kirish xato');Natija:
ERROR: new row for relation "auth_hodisalar" violates check constraint "auth_hodisalar_tur_check""Yangi qator auth_hodisalar_tur_check cheklovini buzadi". tur — faqat kichik harf, raqam va _ (2…40 belgi). Hodisa turlari dasturda ro'yxat sifatida turadi, baza esa formatni kafolatlaydi.
9. Ortiqcha maydonlar va chegaralar
Hujumchi ro'yxat tanasiga "rol": "admin" qo'shsa va kod tanani to'g'ridan bazaga yozsa — u admin bo'ladi. Bu mass assignment, u Backend'dagi injection va mass assignment darsida chuqur keladi. Kanon sxemasi z.strictObject noma'lum maydonni jim tashlamaydi, 422 qaytaradi: kutilmagan maydon — yoki frontend xatosi, yoki hujum.
Kanon testi login'ga g'alati so'rovlar yuboradi:
| So'rov | Javob | Nega |
|---|---|---|
| 1 MB parol | 413 | Express tana chegarasi, argon2 gacha yetmaydi |
| 600 belgili parol | 422 | login sxemasi 512 dan uzunini rad etadi |
"parol": {"$ne": null} |
422 | parol satr bo'lishi shart (NoSQL injection naqshi) |
Content-Type: text/plain |
415 | faqat JSON qabul qilinadi |
GET /v1/auth/kirish |
405 + Allow: POST |
metod yo'q |
10. Hujumchi nigohi
| Hujum | Kanon himoyasi | Test: kutilgan natija |
|---|---|---|
| xato matnidan email bilish | bitta 401 | uch holat — bitta tana |
| javob vaqtidan email bilish | soxtaHash() |
farq ≤ 15 ms |
| ro'yxatdan email bilish | hali ochiq (409) | email tasdiqlash darsida |
qwertyuiopasdfgh bilan ro'yxat |
taqiqlangan ro'yxat | → 422 |
tanaga rol: admin |
z.strictObject |
→ 422 |
| bitta IP dan minglab urinish | jurnal; cheklov keyin | SQL: IP bo'yicha 30 xato |
11. Ko'p uchraydigan xatolar
| Xato | Nega xavfli | To'g'risi |
|---|---|---|
| "Bunday foydalanuvchi yo'q" xabari | enumeration | "Email yoki parol noto'g'ri" |
| "katta harf + raqam + belgi" talabi | Parol123! larni ko'paytiradi |
uzunlik + taqiqlangan ro'yxat |
| login'da ham siyosatni tekshirish | eski hisoblar kira olmaydi | faqat uzunlik chegarasi |
| jurnalga noto'g'ri parolni yozish | log sizsa — parollar ham sizadi | faqat sabab |
| 5 xatodan keyin qattiq lockout | hujumchi qurbonni bloklaydi | tezlik cheklovi, kechikish |
| timing'ni ketma-ket o'lchash | yuklama bitta to'plamga tushadi | navbatma-navbat o'lchash |
12. Mashqlar
Mashqlar kurs/mashqlar/28/05-royxat-login/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
- NIST SP 800-63B-4 parolda kamida bitta raqam va bitta katta harf bo'lishini talab qiladi.
- Login sxemasi ham ro'yxat sxemasidagi 15 belgi qoidasini tekshirishi kerak.
- Javob matni bir xil bo'lsa, user enumeration butunlay yopiladi.
- HIBP k-anonymity'da xizmat SHA-1 hash'ning faqat birinchi 5 belgisini oladi.
- Muvaffaqiyatsiz urinishda noma'lum email ham jurnalga yozilishi kerak — tergov uchun.
Yechim
- Noto'g'ri — NIST tarkib qoidalarini taqiqlaydi.
- Noto'g'ri — siyosat o'zgarsa, eski hisoblar kira olmay qoladi.
- Noto'g'ri — javob vaqti ham bir xil bo'lishi kerak (zaif kodda 38.2 va 5.2 ms); ro'yxat endpoint'i ham email borligini aytmasligi kerak.
- To'g'ri — qolgan 35 belgi bizda qoladi.
- Noto'g'ri — kanon faqat sababni (
email-yoq) va IP ni yozadi.
2-mashq (o'rta): parol siyosati
parolSababi(parol, shaxsiy) funksiyasini yozing. Parolni NFC ga keltiring. Birinchi topilgan muammoni qaytaring: 15 dan qisqa — kamida 15 belgi; 128 dan uzun — 128 belgidan oshmasin; 3 tadan kam turli belgi — turli belgilar juda kam; shaxsiy dagi email yoki ismning 4 va undan uzun bo'lagi parol ichida bo'lsa — email yoki ism bor (registrga befarq). Muammo bo'lmasa — ok. Oltita parolda sinang; chiqishda parolning birinchi 22 belgisini ko'rsating. Ishora: bo'laklash — split(/[@\s.]+/), kesish — slice va padEnd (Kesish, bo'lish va to'ldirish).
Yechim
const MIN = 15;
const MAKS = 128;
function parolSababi(parol, shaxsiy = []) {
const p = parol.normalize("NFC");
if (p.length < MIN) return `kamida ${MIN} belgi`;
if (p.length > MAKS) return `${MAKS} belgidan oshmasin`;
const kichik = p.toLowerCase();
if (new Set(kichik).size <= 3) return "turli belgilar juda kam";
for (const s of shaxsiy) {
const bolaklar = s.toLowerCase().split(/[@\s.]+/)
.filter((b) => b.length >= 4);
if (bolaklar.some((b) => kichik.includes(b))) {
return "email yoki ism bor";
}
}
return "ok";
}
const MEN = ["dilnoza@example.com", "Dilnoza Rahimova"];
const SINOV = [
"Toshkent2026",
"zzzzzzzzzzzzzzzzzzzz",
"dilnoza-rahimova-1990",
"Rahimova bahorda choy ichadi",
"x".repeat(129),
"Qovun pishdi, ariq to'ldi",
];
for (const p of SINOV) {
console.log(p.slice(0, 22).padEnd(22), "→", parolSababi(p, MEN));
}Konsolda:
Toshkent2026 → kamida 15 belgi
zzzzzzzzzzzzzzzzzzzz → turli belgilar juda kam
dilnoza-rahimova-1990 → email yoki ism bor
Rahimova bahorda choy → email yoki ism bor
xxxxxxxxxxxxxxxxxxxxxx → 128 belgidan oshmasin
Qovun pishdi, ariq to' → okTo'rtinchi qatorga qarang: Rahimova bahorda choy ichadi — uzun va yaxshi ibora, lekin familiya bor. Qoida qattiq, lekin mantiqiy: hujumchi Dilnozaning familiyasini biladi va lug'atiga birinchi qo'shadi. dilnoza bo'lagi ikki joydan keladi (emaildan ham, ismdan ham) — some birinchi mosda to'xtaydi.
3-mashq (qiyin): k-anonymity tekshiruvi
necha(parol, soraXizmat) funksiyasini yozing: parolning SHA-1 hash'ini (katta harfli hex) hisoblang, soraXizmat(boshi) ga faqat birinchi 5 belgini bering. Xizmat QOLDIQ:SONI qatorlarini \r\n bilan ajratib qaytaradi. Sizdagi qoldiq ro'yxatda bo'lsa — sonini, bo'lmasa 0 qaytaring. Haqiqiy xizmat o'rniga soxta javob yozing: 21BD1 prefiksi uchun uchta qator, ulardan biri P@ssw0rd ga mos keladi (soni — o'zingiz tanlagan namuna raqam). Ishora: hash — oldingi bo'limdagi sha1Hex; qatorni bo'lish — split(":") (Massiv destructuring).
Yechim
async function sha1Hex(matn) {
const h = await crypto.subtle.digest(
"SHA-1", new TextEncoder().encode(matn));
return Array.from(new Uint8Array(h), (b) =>
b.toString(16).padStart(2, "0")).join("").toUpperCase();
}
// "Server" — 21BD1 prefiksi uchun soxta javob (raqamlar namuna)
const SOXTA_JAVOB = {
"21BD1": [
"0018A45C4D1DEF81644B54AB7F969B88D65:3",
"2DC183F740EE76F27B78EB39C8AD972A757:52579",
"F2B6B4E5E5E8B49E2D8D5C9A0A2B6C1D3E4:1",
].join("\r\n"),
};
async function necha(parol, soraXizmat) {
const hash = await sha1Hex(parol);
const matn = await soraXizmat(hash.slice(0, 5));
for (const qator of matn.split("\r\n")) {
const [qoldiq, soni] = qator.split(":");
if (qoldiq === hash.slice(5)) return Number(soni);
}
return 0;
}
const xizmat = async (boshi) => SOXTA_JAVOB[boshi] ?? "";
console.log(await necha("P@ssw0rd", xizmat));
console.log(await necha("Qovun pishdi, ariq to'ldi", xizmat));Konsolda:
52579
052579 — bizning soxta javobimizdagi namuna raqam, haqiqiy statistika emas. Muhimi tuzilish: tarmoqqa faqat 21BD1 chiqdi. soraXizmat ni parametr qilib berdik — haqiqiy loyihada u fetch bilan HIBP ga boradi, testda esa soxta funksiya qo'yiladi (Mock, stub, spy va fake). Ikkinchi parolning prefiksi boshqa — soxta xizmat bo'sh satr qaytaradi va natija 0.
13. Loyiha qadami: ro'yxat, login va jurnal
13.1 Migratsiya
sql/111-auth-hodisalar.sql va xuddi shu SQL Prisma migratsiyasi sifatida — prisma/migrations/20261027110000_auth_hodisalar/migration.sql:
-- Auth jurnali: hujumni ko'rish (credential stuffing — bir IP dan
-- ko'p email, brute force — bitta hisobga ko'p urinish).
-- Parol, token, kod — HECH QACHON yozilmaydi.
CREATE TABLE auth_hodisalar (
id bigint GENERATED ALWAYS AS IDENTITY,
foydalanuvchi_id bigint, -- noma'lum email — NULL
tur text NOT NULL,
ip inet,
user_agent text,
tafsilot jsonb NOT NULL DEFAULT '{}',
vaqt timestamptz NOT NULL DEFAULT now(),
CONSTRAINT auth_hodisalar_pkey PRIMARY KEY (id),
CONSTRAINT auth_hodisalar_foydalanuvchi_id_fkey
FOREIGN KEY (foydalanuvchi_id) REFERENCES foydalanuvchilar (id)
ON DELETE CASCADE,
CONSTRAINT auth_hodisalar_tur_check
CHECK (tur ~ '^[a-z][a-z0-9_]{1,39}$'),
CONSTRAINT auth_hodisalar_user_agent_check
CHECK (char_length(user_agent) <= 300)
);
CREATE INDEX auth_hodisalar_foydalanuvchi_idx
ON auth_hodisalar (foydalanuvchi_id, vaqt DESC);
CREATE INDEX auth_hodisalar_ip_idx ON auth_hodisalar (ip, vaqt DESC);Kanon izohini qisqartirdik va qatorlarni bo'ldik; faylda COMMENT ON TABLE ham bor. schema.prisma ga AuthHodisa modeli (@@map("auth_hodisalar"), ikkala indeks bilan) va Foydalanuvchi ga authHodisas AuthHodisa[] qo'shiladi. Shunda migratsiya va sxema farqsiz qoladi (Prisma Migrate chuqur).
13.2 src/auth/sxema.ts
Darsdagi hamma kod — telefon uchun bo'lingan, izohlari qisqartirilgan; ma'nosi kanondagidek. Bo'lingan nusxani kanon holatiga qo'yib, tip, lint va auth testlaridan o'tkazdik.
// sxema.ts — auth so'rovlari sxemalari. z.strictObject: noma'lum
// maydon (`rol`) — 422 (mass assignment). Parol siyosati — NIST
// SP 800-63B-4: 15+ belgi, tarkib qoidalari YO'Q, blocklist, NFC.
import { z } from "zod";
import { taqiqlanganmi } from "./taqiqlangan-parollar.ts";
export const PAROL_MIN = 15;
export const PAROL_MAKS = 128;
/** Email: bo'shliqlar olinadi, kichik harf (bazada lower(email)
* noyob), uzunlik RFC 5321 chegarasi */
export const EmailSxemasi = z
.string()
.trim()
.toLowerCase()
.max(254, { error: "email juda uzun" })
.pipe(z.email({ error: "email noto'g'ri" }));
/** Parol: NFC (Ў bitta belgi ham, У + ˘ ham bir xil hash beradi),
* trim QILINMAYDI (bo'shliq ham belgi) */
const Parol = z
.string()
.transform((p) => p.normalize("NFC"))
.pipe(
z
.string()
.min(PAROL_MIN, {
error:
`parol kamida ${String(PAROL_MIN)} belgi ` +
"(NIST SP 800-63B-4). Bir nechta so'zdan iborat ibora " +
"— eslab qolish oson",
})
.max(PAROL_MAKS, {
error: `parol ${String(PAROL_MAKS)} belgidan oshmasin`,
}),
);
const ToliqIsm = z
.string()
.trim()
.normalize("NFC")
.min(1, { error: "ism bo'sh" })
.max(100, { error: "ism 100 belgidan oshmasin" });
export const RoyxatSxemasi = z
.strictObject({
email: EmailSxemasi,
toliqIsm: ToliqIsm,
parol: Parol,
})
.superRefine((x, ctx) => {
const sabab = taqiqlanganmi(x.parol, [x.email, x.toliqIsm]);
if (sabab !== undefined)
ctx.issues.push({
code: "custom",
input: x.parol,
path: ["parol"],
message: sabab,
});
});
export type Royxat = z.output<typeof RoyxatSxemasi>;
/** Login: parol siyosati TEKSHIRILMAYDI (eski, qisqa parolli hisob
* ham kira olsin) — faqat uzunlik chegarasi (DoS: 1 MB parolni
* hash qilmaymiz) */
export const KirishSxemasi = z.strictObject({
email: EmailSxemasi,
parol: z
.string()
.min(1, { error: "parol bo'sh" })
.max(PAROL_MAKS * 4, { error: "parol juda uzun" })
.transform((p) => p.normalize("NFC")),
});
export type Kirish = z.output<typeof KirishSxemasi>;superRefine — butun obyekt tekshirilgandan keyin ishlaydi, shuning uchun parolni email va ism bilan solishtira oladi. Xato path: ["parol"] bilan qo'shiladi — javobda body.parol maydoni ko'rinadi, frontend xatoni to'g'ri maydon ostiga qo'yadi.
13.3 src/auth/taqiqlangan-parollar.ts
ketmaKetmi funksiyasi — «Taqiqlangan parollar» bo'limidagi bilan bir xil (faqat p: string turi bor), ro'yxatning faqat boshini ko'rsatdik:
// taqiqlangan-parollar.ts — keng tarqalgan / taxmin qilinadigan
// parollar (NIST: "blocklist"). To'liq ro'yxat (HIBP) — tashqi
// xizmat; kursda SINALMAGAN. Bu yerda: kichik lokal ro'yxat +
// qoidalar (takror, ketma-ketlik, shaxsiy ma'lumot, xizmat nomi).
// 15+ belgili mashhur parollar (kichik harfda solishtiriladi)
const ROYXAT = new Set([
"123456789012345", "1234567890123456", "qwertyuiopasdfgh",
// … jami 25 ta
]);
const XIZMAT = ["vazifalar", "ilmhamroh", "vazifalar-api"];
// function ketmaKetmi(p: string): boolean { … }
/**
* @param shaxsiy foydalanuvchining o'z ma'lumoti (email, ism) —
* parolda bo'lmasin
* @returns sabab matni yoki undefined (ruxsat)
*/
export function taqiqlanganmi(
parol: string,
shaxsiy: readonly string[] = [],
): string | undefined {
const p = parol.toLowerCase();
if (ROYXAT.has(p)) {
return "bu parol keng tarqalgan (sizib chiqqan ro'yxatlarda " +
"bor) — boshqasini tanlang";
}
if (new Set(p).size <= 3) return "parolda turli belgilar juda kam";
if (ketmaKetmi(p)) return "parol oddiy ketma-ketlik (abc…, 123…)";
for (const s of XIZMAT) {
if (p.includes(s)) return "parolda xizmat nomi bo'lmasin";
}
for (const s of shaxsiy) {
const bolak = s
.toLowerCase()
.split(/[@\s.]+/)
.filter((x) => x.length >= 4);
for (const b of bolak) {
if (p.includes(b)) {
return "parolda email yoki ismingiz bo'lmasin";
}
}
}
return undefined;
}13.4 src/auth/ombor.ts
AuthOmbori — pg bilan yozilgan kichik klass (Prisma emas: u ikkala saqlash rejimida, postgres va prisma, bitta ulanishlar hovuzini — pool'ni ishlatadi). Unda beshta metod bor: emailBilan, idBilan, yarat, parolniYoz, hodisa; har so'rov 26-qismdagi TURLAR bilan (bigint → number). Xavfsizlik uchun muhim uchtasi:
export type HodisaTuri =
| "royxat"
| "kirish"
| "kirish_xato"
| "parol_qayta_hash";
const USTUNLAR =
'id, email, toliq_ism AS "toliqIsm", parol_hash AS "parolHash"';
// … AuthOmbori klassi ichida:
/** Registrga befarq (lower(email) noyob indeksi shu ifoda bilan —
* indeks ishlatiladi) */
async emailBilan(email: string): Promise<HisobQatori | undefined> {
const [q] = await this.#sorov<HisobQatori>(
`SELECT ${USTUNLAR} FROM foydalanuvchilar ` +
"WHERE lower(email) = lower($1)",
[email],
);
return q;
}
/** @throws EmailBandXatosi — shu email (registrsiz) allaqachon bor
* (23505 foydalanuvchilar_email_key) */
async yarat(y: {
email: string;
toliqIsm: string;
parolHash: string | null;
}): Promise<Foydalanuvchi> {
try {
const [q] = await this.#sorov<HisobQatori>(
"INSERT INTO foydalanuvchilar " +
"(email, toliq_ism, parol_hash) " +
`VALUES ($1, $2, $3) RETURNING ${USTUNLAR}`,
[y.email, y.toliqIsm, y.parolHash],
);
if (q === undefined) throw new Error("INSERT qator qaytarmadi");
return { id: q.id, email: q.email, toliqIsm: q.toliqIsm };
} catch (x) {
if (
x instanceof pg.DatabaseError &&
x.code === "23505" &&
x.constraint === "foydalanuvchilar_email_key"
) {
throw new EmailBandXatosi();
}
throw x;
}
}Qolgan qismi — turlar, EmailBandXatosi klassi, #sorov, idBilan, parolniYoz va hodisa (bitta parametrli INSERT); ular 26-qismdagi PgOmbori naqshini takrorlaydi. Email bandligi bazadan bilinadi — 23505 va aniq cheklov nomi. Avval SELECT, keyin INSERT qilish poygaga ochiq bo'lardi. hodisa ichida user_agent 300 belgida kesiladi — bazadagi CHECK bilan bir xil.
13.5 src/auth/xizmat.ts
// xizmat.ts — auth biznes qoidalari: ro'yxat va login (HTTP'ni
// bilmaydi). Login: javob bir xil, vaqt bir xil (soxtaHash), har
// urinish jurnalga.
import { IlovaXatosi, KonfliktXatosi } from "../umumiy/xatolar.ts";
import { parolHash, parolTekshir, soxtaHash } from "./parol.ts";
import { EmailBandXatosi } from "./ombor.ts";
import type { AuthOmbori, Foydalanuvchi } from "./ombor.ts";
import type { Kirish, Royxat } from "./sxema.ts";
/** So'rov konteksti — jurnal uchun (IP, brauzer) */
export type Mijoz = {
ip?: string | undefined;
userAgent?: string | undefined;
};
export class KirishXatosi extends IlovaXatosi {
constructor() {
super(401, "Email yoki parol noto'g'ri", {
kod: "kirish-xato",
sarlavha: "Kirish muvaffaqiyatsiz",
});
}
}
export function authXizmati(ombor: AuthOmbori) {
return {
async royxat(r: Royxat, mijoz: Mijoz): Promise<Foydalanuvchi> {
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,
});
return f;
} catch (x) {
// Hozircha 409 — email band ekanini OSHKOR qiladi
// (enumeration). Email tasdiqlash bilan bir xil javobga
// o'tadi
if (x instanceof EmailBandXatosi) {
throw new KonfliktXatosi("Bu email bilan hisob bor", {
kod: "email-band",
sarlavha: "Email band",
});
}
throw x;
}
},
async kirish(k: Kirish, mijoz: Mijoz): Promise<Foydalanuvchi> {
const hisob = await ombor.emailBilan(k.email);
// Topilmasa yoki parolsiz hisob bo'lsa ham argon2 bajariladi —
// javob vaqti bir xil
const n = await parolTekshir(
hisob?.parolHash ?? (await soxtaHash()),
k.parol,
);
if (
hisob === undefined ||
hisob.parolHash === null ||
!n.togri
) {
await ombor.hodisa({
tur: "kirish_xato",
foydalanuvchiId: hisob?.id,
...mijoz,
tafsilot: {
sabab:
hisob === undefined
? "email-yoq"
: hisob.parolHash === null
? "parolsiz-hisob"
: "parol",
},
});
throw new KirishXatosi();
}
if (n.qaytaHash) {
await ombor.parolniYoz(hisob.id, await parolHash(k.parol));
await ombor.hodisa({
tur: "parol_qayta_hash",
foydalanuvchiId: hisob.id,
...mijoz,
});
}
await ombor.hodisa({
tur: "kirish",
foydalanuvchiId: hisob.id,
...mijoz,
});
return {
id: hisob.id,
email: hisob.email,
toliqIsm: hisob.toliqIsm,
};
},
};
}
export type AuthXizmati = ReturnType<typeof authXizmati>;kirish ichidagi tartibga qarang: avval argon2, keyin shart. Shartni oldinga qo'ysangiz ("hisob yo'q — darhol xato"), timing teshigi qaytadi. hisob.parolHash === null (parolsiz hisob) ham soxta hash bilan tekshiriladi — bu holat ham tashqaridan oddiy "noto'g'ri parol" kabi ko'rinadi. Oxirida — oldingi darsdagi rehash on login: parametrlar eskirgan bo'lsa, yangi hash yoziladi va jurnalga parol_qayta_hash tushadi.
13.6 src/auth/marshrut.ts va ulanish
Router 25-qismdagi naqshda: jsonKerak (faqat JSON, aks holda 415), tekshirib (Zod, aks holda 422), boshqa metodlarga 405. Faylning muhim qismi:
export function mijoz(req: Request): Mijoz {
return { ip: req.ip, userAgent: req.get("user-agent") };
}
export function authRouteri(xizmat: AuthXizmati): Router {
const r = Router();
// Auth javoblari keshlanmasin (proxy, brauzer "orqaga" tugmasi)
r.use((_req, res, next) => {
res.set("Cache-Control", "no-store");
next();
});
r.route("/kirish")
.post(
jsonKerak,
tekshirib(
{ body: KirishSxemasi },
async ({ body }, req, res) => {
const f = await xizmat.kirish(body, mijoz(req));
res.json({ foydalanuvchi: f });
},
),
)
.all(metodYoq("POST"));
// /royxat — xuddi shunday: RoyxatSxemasi, xizmat.royxat, 201
return r;
}req.ip — Express trust proxy sozlamasiga bog'liq (Reverse proxy orqasida Node): proksi ortida u X-Forwarded-For dan olinadi. Jurnalning IP ustuni shunga tayanadi, shuning uchun TRUST_PROXY ni faqat haqiqatan proksi ortida yoqing — aks holda hujumchi sarlavhani o'zi yozib, boshqa IP nomidan "urinadi".
Router uch faylda ulanadi (diff qisqartirilgan va bo'lingan):
// src/ilova.ts — Bogliqliklar turi va v1 router
+ auth?: AuthOmbori | undefined;
+ if (auth !== undefined) {
+ v1.use("/auth", authRouteri(authXizmati(auth)));
+ }
// src/ishga-tushir.ts — postgres va prisma rejimi pool'ni ham beradi
- return { ombor: new PgOmbori(pool, config.EGASI_ID), yop: … };
+ return { ombor: new PgOmbori(pool, config.EGASI_ID), yop: …,
+ pool };
// src/server.ts — bogliqliklar ichida
+ auth: saqlash.pool === undefined
+ ? undefined
+ : new AuthOmbori(saqlash.pool),Xotira va fayl rejimida pool yo'q — demak auth ham yo'q, /v1/auth yo'llari ulanmaydi.
13.7 Testlar
test/auth.test.ts — 10 ta test, test/auth-yordam.ts — alohida test bazasi (TEST_AUTH_DATABASE_URL, sukut vazifalar_test_auth) va tozala() (hamma jadvalni TRUNCATE). Eng muhimi:
test("enumeration yo'q: noto'g'ri parol, noma'lum email, " +
"parolsiz hisob — bir xil 401 tanasi; jurnalda sabab " +
"alohida", async () => {
const app = ilova();
const tanalar = [];
for (const body of [
{ email: "aziz@example.com", parol: "noto'g'ri-parol-123" },
{ email: "yoq@example.com", parol: PAROL },
{ email: "shahnoza@example.com", parol: PAROL },
]) {
const r = await request(app).post("/v1/auth/kirish")
.send(body).expect(401);
const tana = { ...(r.body as Record<string, unknown>) };
delete tana["sorovId"]; // har so'rovda boshqa
tanalar.push(JSON.stringify(tana));
}
assert.equal(new Set(tanalar).size, 1, tanalar.join("\n"));
assert.deepEqual(
(await hodisalar()).map((h) =>
[h.tur, h.foydalanuvchi_id, h.sabab]),
[
["kirish_xato", 1, "parol"],
["kirish_xato", null, "email-yoq"],
["kirish_xato", 2, "parolsiz-hisob"],
],
);Test bir vaqtda ikki narsani tekshiradi: tashqarida javob bir xil, ichkarida (jurnalda) sabab har xil. Timing testi ketma-ket o'lchaydi va "noma'lum email 15 ms dan sekin" ekanini ham talab qiladi — ya'ni argon2 haqiqatan ishladi. Qolgan testlar: taqiqlangan parollar, 201 va jurnal (IP — X-Forwarded-For dan), uch xil 422, mass assignment, 409, registr va bo'shliq, rehash on login, chegaralar.
Faqat shu fayl testlarini ishga tushiramiz (biz ishga tushirdik: kanon holat 05 nusxasi va yuqoridagi bo'lingan fayllar bilan, Node 24.21.0, PostgreSQL 17.6, uchinchi urinish):
node --test test/auth.test.ts✔ taqiqlangan parollar — ro'yxat, takror, ketma-ketlik, shaxsiy ma'lumot, xizmat nomi (1.5105ms)
✔ POST /v1/auth/royxat (1606.3836ms)
▶ POST /v1/auth/kirish
✔ 200 — to'g'ri parol (email registri va bo'shliq farqsiz) (505.1243ms)
✔ enumeration yo'q: noto'g'ri parol, noma'lum email, parolsiz hisob — bir xil 401 tanasi; jurnalda sabab alohida (495.6531ms)
✔ timing: noma'lum email ham argon2 bajaradi — mediana farqi kichik (≤ 15 ms) (1068.9182ms)
✔ rehash on login: eski parametrli hash — kirishda yangisiga almashadi (264.7095ms)
✔ chegaralar: 1 MB parol — 413 (tana chegarasi), 600 belgi — 422 (hash qilinmaydi), noto'g'ri tur — 422, Content-Type — 415 (250.7286ms)
✔ POST /v1/auth/kirish (2586.2761ms)
ℹ tests 10
ℹ pass 10
ℹ fail 0Chiqishni qisqartirdik: ro'yxat to'plamining to'rtta ichki qatori va ba'zi ℹ qatorlari olib tashlandi.
13.8 Tekshiruv va commit
Butun loyiha tekshiruvi (kanon, 2026-10-27, PostgreSQL 18.6). Tip va lint xatosiz, testlar 128 (127 + 1 o'tkazilgan), Prisma migratsiyasi sql/ bilan bir xil. Jonli API uch rejimda (postgres, prisma, prisma + Redis kesh): login 200 va 401.
M="feat(auth): ro'yxat va login — NIST parol siyosati, bir xil"
M+=" javob va vaqt, auth jurnali; db: 111 auth_hodisalar"
git add src test sql prisma
git commit -m "$M"Bu darsda login hali sessiya bermaydi: server "ha, bu Aziz" deydi, lekin keyingi so'rovda Azizni tanimaydi. Kimlikni so'rovdan so'rovga olib o'tish — cookie va sessiya ishi.
14. Real ishda
- Katta saytlar login'ni bir necha qatlam bilan himoyalaydi: tezlik cheklovi, shubhali kirishda CAPTCHA, sizgan parollar tekshiruvi.
- "Email yoki parol noto'g'ri" biroz noqulay — yonidagi "Parolni unutdingizmi?" havolasi buni yumshatadi.
- Jurnal ogohlantirish tizimiga ulanadi ("bitta IP dan 15 daqiqada 100 ta xato").
- Intervyuda: "User enumeration qanday yopiladi?", "Lockout nega xavfli?".
Xulosa
- Login'ga hujumlar: brute force, credential stuffing, password spraying, user enumeration.
- Email —
trim+ kichik harf, bazadalower(email)noyob indeksi kafolat beradi. - NIST SP 800-63B-4: bir faktorli hisobda kamida 15 belgi, 64 gacha ruxsat, tarkib qoidalari va davriy almashtirish yo'q, taqiqlangan ro'yxat bor, NFC.
- Uch xil xato — bitta 401 tanasi; email topilmasa ham
soxtaHash()bilan argon2 — zaif kodda 38.2 va 5.2 ms, kanonda 65.6 va 64.4 ms. - Timing'ni navbatma-navbat o'lchang; ketma-ket o'lchaydigan test band mashinada beqaror.
- Qattiq lockout — DoS; o'rniga tezlik cheklovi va kechikish. Har urinish
auth_hodisalarga yoziladi, sir va noma'lum email yozilmaydi.
Keyingi dars: Cookie chuqur — login'dan keyin server foydalanuvchini qanday "eslab qoladi": Set-Cookie, HttpOnly, Secure, SameSite va __Host- prefiksi haqiqiy brauzerda.
Manbalar
- NIST SP 800-63B-4 (2025-yil iyul), «Password Verifiers» va «Rate Limiting (Throttling)» bo'limlari: https://pages.nist.gov/800-63-4/sp800-63b.html (2026-10-28 da o'qildi)
- OWASP Authentication Cheat Sheet (xato xabarlari, enumeration, lockout): https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- OWASP Credential Stuffing Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Credential_Stuffing_Prevention_Cheat_Sheet.html
- Have I Been Pwned — Pwned Passwords API (range, k-anonymity): https://haveibeenpwned.com/API/v3#PwnedPasswords
- OWASP Top 10:2025 — A09 Security Logging and Alerting Failures: https://owasp.org/Top10/
- Kanon o'lchovi (IlmHamroh, 2026-10-27): login javob vaqti tajribasi (Node 24.21.0, PostgreSQL 18.6, supertest, i5-12500H), holat 05 tekshiruvi. Brauzer misollari, SQL so'rovlar (PostgreSQL 17.6) va
auth.test.ts— biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!