Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. siteverify: tokenni serverda tekshirish
- 2.1 Oqim
- 2.2 Haqiqiy javoblar — rasmiy test kalitlari bilan
- 2.3 Topilma: test kaliti va hostname
- 3. Kanon dizayni: qayerda, qanday va xato bo'lsa nima
- 3.1 Turnstile klassi
- 3.2 Yopiq yiqilish
- 3.3 Qaysi yo'llarda
- 4. Asal idish (honeypot)
- 4.1 G'oya va "bir xil javob"
- 4.2 Sxemadagi maydon
- 5. SMS pumping
- 5.1 Hujum
- 5.2 To'rt qatlam
- 5.3 Byudjet tugasa — signal
- 6. Boshqa CAPTCHA'lar
- 7. Hujumchi nigohi
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): qaysi javob?
- 2-mashq (o'rta): xatolardan keyin CAPTCHA
- 3-mashq (qiyin): kunlik byudjet va prefiks
- 10. Loyiha qadami: captcha.ts, asal idish va SMS byudjeti
- 10.1 src/auth/captcha.ts
- 10.2 Marshrut, sxema va byudjet
- 10.3 Testlar
- 10.4 Tekshiruv va commit
- 11. Real ishda
- Xulosa
- Manbalar
CAPTCHA tokenini serverda tekshirish: Turnstile siteverify, asal idish va SMS pumping
Qisqacha: CAPTCHA vidjeti brauzerda token beradi, lekin token o'zi hech narsani isbotlamaydi. Server uni maxfiy kalit bilan
siteverifyga yuborib tekshirishi shart.vazifalar-apida Turnstile tokeniX-Captchasarlavhasida keladi va faqat bot uchun "foydali" yo'llarda talab qilinadi: ro'yxat, parol tiklash so'rovi va SMS kod. Tarmoq xatosida server rad etadi (yopiq yiqilish). Kanon Cloudflare'ning rasmiy test kalitlari bilan haqiqiysiteverifyni chaqirib, kutilmagan narsani topdi: test kalitihostname: "example.com"qaytaradi — domen tekshiruvi bilan birga ishlamaydi. Yana ikki qatlam: yashirin asal idish (honeypot) maydoni va SMS'ga kunlik global byudjet — pulga tushiradigan "SMS pumping" hujumiga qarshi.
Bu darsda
- CAPTCHA tokeni nega serverda tekshirilishini va
siteverifyjavobini qanday o'qishni tushuntirasiz. - Rasmiy test kalitlari bilan haqiqiy javoblarni ko'rib,
hostnametuzog'ini bilasiz. - Yopiq yiqilish (fail closed) va CAPTCHA'ni qaysi yo'llarga qo'yish qarorini asoslaysiz.
- Asal idish maydonini "bir xil javob" tamoyili bilan yozasiz.
- SMS pumping hujumini va unga qarshi to'rt qatlamni tushuntirasiz;
vazifalar-apiga bot himoyasini va 4 ta testni qo'shasiz.
Oldin bilishingiz kerak: CAPTCHA vidjetini frontendga qo'shish: Turnstile va reCAPTCHA, OTP orqali login, Rate limiting, Server Actions xavfsizligi (asal idish).
Oldingi darsda bitta hujumchi ko'p so'rov bilan serverni band qilmoqchi edi. Bugungi hujumchi boshqacha: u serverni yiqitmaydi, uni ishlatadi — minglab soxta hisob ochadi, begona emailga xat yuboradi, sizning hisobingizdan SMS jo'natadi. Har so'rov "to'g'ri", faqat uni odam emas, dastur yuborgan.
1. Nega bu kerak?
21-qismda CAPTCHA vidjeti ni formaga qo'shgansiz: vidjet "odammi?" deb baholaydi va formaga token beradi. O'sha darsda bir qoida bor edi: tekshirilmagan token — himoya emas. Bugun o'sha tekshiruvni backend'da yozamiz.
Nega frontend yetmaydi? Chunki bot sizning formangizni umuman ochmaydi. U POST /v1/auth/royxat ga to'g'ridan-to'g'ri so'rov yuboradi — curl bilan. Vidjet ham, JavaScript ham, tugma ham yo'q. Server tokenni talab qilmasa — CAPTCHA faqat odamlarga xalaqit beradi, botlarga emas.
vazifalar-api da botlar uchun "qimmatli" uchta yo'l bor:
| Yo'l | Bot nima qiladi | Kim zarar ko'radi |
|---|---|---|
POST /royxat |
minglab soxta hisob, spam | baza, boshqa foydalanuvchilar |
POST /parol-tiklash/sorov |
begona emailga xat oqimi | email obro'yingiz, qurbon |
POST /otp/yubor |
SMS yuborish | sizning hamyoningiz |
Uchinchisi — eng xavflisi. Uni alohida bo'limda ko'ramiz.
2. siteverify: tokenni serverda tekshirish
2.1 Oqim
sequenceDiagram
participant B as Brauzer
participant C as Cloudflare
participant A as vazifalar-api
B->>C: vidjet (sayt kaliti)
C-->>B: token
B->>A: POST /royxat, X-Captcha: token
A->>C: siteverify: maxfiy kalit + token
C-->>A: success, hostname, error-codes
A-->>B: 202 yoki 400 captchaServer Cloudflare'ning https://challenges.cloudflare.com/turnstile/v0/siteverify manziliga POST yuboradi. Tana — forma formatida uchta maydon:
const tana = new URLSearchParams({
secret: "sir",
response: "tok",
remoteip: "203.0.113.5",
});
console.log(tana.toString());
const ip = undefined;
const ipsiz = new URLSearchParams({
secret: "sir",
response: "tok",
...(ip === undefined ? {} : { remoteip: ip }),
});
console.log(ipsiz.toString());Konsolda:
secret=sir&response=tok&remoteip=203.0.113.5
secret=sir&response=toksecret— maxfiy kalit (secret key): faqat serverda,.envda. Brauzerga hech qachon chiqmaydi.response— brauzerdan kelgan token.remoteip— foydalanuvchi IP'si, ixtiyoriy. Ikkinchi qatordagi naqsh kanondan: IP noma'lum bo'lsa, maydon umuman yuborilmaydi (undefinedsatri emas).
Kanon testi aynan shu tanani kutadi: secret=sir&response=tok&remoteip=203.0.113.5.
2.2 Haqiqiy javoblar — rasmiy test kalitlari bilan
Cloudflare hujjatida (2026-10 holatida) sinov uchun ochiq test maxfiy kalitlari bor. Ular har qanday tokenga oldindan belgilangan javob qaytaradi. Kanon Cloudflare'da hisob ochmadi va haqiqiy kalit ishlatmadi — faqat shu test kalitlari bilan haqiqiy siteverify ga so'rov yubordi. Token — test vidjeti beradigan soxta satr, IP yuborilmadi:
const URL =
"https://challenges.cloudflare.com/turnstile/v0/siteverify";
for (const sir of ["1x0000000000000000000000000000000AA",
"2x0000000000000000000000000000000AA",
"3x0000000000000000000000000000000AA"]) {
const r = await fetch(URL, {
method: "POST",
body: new URLSearchParams({
secret: sir, response: "XXXX.DUMMY.TOKEN.XXXX",
}),
signal: AbortSignal.timeout(10_000),
});
const j = await r.json();
console.log(sir.slice(0, 2), r.status, JSON.stringify(j));
}Bu — kanon skriptining qisqartirilgan nusxasi. Quyidagi natija to'liq skriptdan (u har kalitga nom va javob vaqtini ham yozadi, bo'sh token bilan to'rtinchi so'rov ham yuboradi; 2026-10-28, Node 24.21.0; uzun JSON qatorlarini qisqartirmadik). Har qatorda HTTP status, success va error-codes ga qarang:
doim o'tadi (1x…AA) HTTP 200, 419 ms → {"challenge_ts":"2026-10-28T11:25:11.164Z","error-codes":[],"hostname":"example.com","metadata":{"result_with_testing_key":true},"success":true}
doim rad (2x…AA) HTTP 200, 105 ms → {"error-codes":["invalid-input-response"],"success":false,"messages":[],"metadata":{"result_with_testing_key":true}}
token ishlatilgan (3x…AA) HTTP 200, 105 ms → {"error-codes":["timeout-or-duplicate"],"success":false,"messages":[],"metadata":{"result_with_testing_key":true}}
bo'sh token bilan (1x) HTTP 200 → {"error-codes":["missing-input-response"],"success":false,"messages":[]}Bu blokni saytda ishga tushirib bo'lmaydi (tarmoq va Cloudflare kerak), natija o'quv tajribasidan. Nimalarni o'qiymiz:
- HTTP status hamisha 200. Hatto rad etilganda ham. Shuning uchun
r.okga qarab qaror qilish — xato. Qaror faqat tanadagisuccessmaydoniga qarab. error-codes— rad sababi:invalid-input-response(token yaroqsiz),timeout-or-duplicate(token eskirgan yoki ikkinchi marta ishlatilgan),missing-input-response(token yo'q).metadata.result_with_testing_key: true— "bu test kaliti javobi". Prodakshn loglarida buni ko'rsangiz — sozlama xato.- Tokenning bir martalik ekani muhim: bir tokenni ikki so'rovda ishlatib bo'lmaydi, va u 300 soniya (5 daqiqa) yashaydi.
2.3 Topilma: test kaliti va hostname
Birinchi qatorga yana qarang: "hostname":"example.com". Haqiqiy javobda hostname — token qaysi saytda olinganini bildiradi. Server uni o'z domeni bilan solishtirsa, boshqa saytda (masalan, hujumchining "token fermasida") olingan tokenni rad etadi. Kanon bu tekshiruvni qo'ydi (TURNSTILE_HOSTNAME).
Lekin test kaliti har doim example.com qaytaradi. Natija: lokal sinovda test kaliti va TURNSTILE_HOSTNAME=vazifalar.uz ni birga bersangiz — hamma rad etiladi, "doim o'tadigan" kalit bilan ham. Kanonning qaror funksiyasini o'sha haqiqiy javoblarga beramiz:
const JAVOBLAR = {
"1x (doim o'tadi)": { success: true, hostname: "example.com",
"error-codes": [], metadata: { result_with_testing_key: true } },
"2x (doim rad)": { success: false,
"error-codes": ["invalid-input-response"] },
"3x (ishlatilgan)": { success: false,
"error-codes": ["timeout-or-duplicate"] },
"bo'sh token": { success: false,
"error-codes": ["missing-input-response"] },
};
function qaror(j, hostname) {
if (j.success !== true) {
const kodlar = j["error-codes"];
return Array.isArray(kodlar) ? kodlar.join(",") : "rad";
}
if (hostname !== undefined && j.hostname !== hostname) {
return "hostname";
}
return "ok";
}
for (const [nom, j] of Object.entries(JAVOBLAR)) {
const uz = qaror(j, "vazifalar.uz");
console.log(nom.padEnd(18), qaror(j), "|", uz);
}Konsolda:
1x (doim o'tadi) ok | hostname
2x (doim rad) invalid-input-response | invalid-input-response
3x (ishlatilgan) timeout-or-duplicate | timeout-or-duplicate
bo'sh token missing-input-response | missing-input-responseBirinchi qatorning o'ng tomoni — tuzoq. Kanon buni .env.example izohiga yozdi: "test kaliti bilan TURNSTILE_HOSTNAME bermang". Saboq kengroq: test muhiti prodakshndan farq qiladi — test kaliti bilan "hammasi ishladi" degani prodakshn sozlamasi to'g'ri degani emas.
Tekshirib ko'ring: Nega
j.success === truedeb yozilgan,if (j.success)emas?
Javob
Javob — tashqi ma'lumot, ishonib bo'lmaydi. if (j.success) "false" satrini, 1 ni yoki bo'sh bo'lmagan har qanday obyektni ham "rost" deb qabul qilardi. === true faqat haqiqiy mantiqiy true ni o'tkazadi. Kanonda javob tipi ham success?: unknown — TypeScript har maydonni tekshirishga majburlaydi.
3. Kanon dizayni: qayerda, qanday va xato bo'lsa nima
3.1 Turnstile klassi
Kanon src/auth/captcha.ts — 70 qatorga yaqin. Asosiy qarorlar:
| Qaror | Kodda | Nega |
|---|---|---|
| token yo'q yoki > 2 048 belgi | so'rov yubormaydi, rad | Cloudflare'ga keraksiz so'rov yo'q; chegara — Cloudflare hujjatida |
AbortSignal.timeout(3000) |
3 s | Cloudflare sekin bo'lsa, server osilmasin |
| tarmoq xatosi | rad (siteverify-xato) |
yopiq yiqilish |
success !== true |
rad, sabab — error-codes |
jurnalda ko'rinadi |
hostname berilgan va mos emas |
rad | begona saytdagi token |
fetchFn — tikuv: testda soxta fetch beriladi, haqiqiy Cloudflare'ga so'rov ketmaydi. Interfeys (CaptchaTekshiruvchi) ham ataylab: ertaga hCaptcha yoki boshqa provayderga o'tsangiz — faqat yangi klass yoziladi, marshrutlar o'zgarmaydi.
3.2 Yopiq yiqilish
Cloudflare javob bermasa nima qilamiz? Ikki yo'l:
- yopiq yiqilish (fail closed) — rad etamiz: odamlar ham vaqtincha ro'yxatdan o'ta olmaydi;
- ochiq yiqilish (fail open) — o'tkazamiz: botlar ham o'tadi.
Kanon CAPTCHA uchun yopiqni tanladi. Qiziq tomoni: Rate limiting darsida tezlik cheklagichi uchun aksincha qaror qabul qilingan — u buzilsa so'rov o'tadi (fail open), jurnalga yoziladi. Nega bir loyihada ikki xil qaror?
- Cheklagich login yo'lida ham turadi. U buzilsa va hamma rad etilsa — hech kim tizimga kira olmaydi, butun ilova to'xtaydi.
- CAPTCHA esa faqat yangi hisob, xat va SMS yo'llarida. Ular bir necha daqiqa ishlamasa — noqulay, lekin ilova ishlaydi. O'tkazib yuborsak esa — bot ayni shu daqiqalarni kutib turgan bo'lishi mumkin, SMS esa pul.
Qaror har yo'l uchun alohida qilinadi: "rad etish qanchaga tushadi" va "o'tkazish qanchaga tushadi".
3.3 Qaysi yo'llarda
Token X-Captcha sarlavhasida keladi va faqat uchta POST yo'lda talab qilinadi: /royxat, /parol-tiklash/sorov, /otp/yubor. CAPTCHA faqat TURNSTILE_SIRI berilganda yoqiladi — lokal ishlab chiqishda va testlarda u o'chiq.
Login — CAPTCHA'siz. Bu ataylab. Login — eng ko'p ishlatiladigan yo'l, har kirishda CAPTCHA odamlarni charchatadi. Login'ni hisob + IP bo'yicha tezlik cheklovi va oldingi darsdagi semafor himoyalaydi. Ko'p saytlar oraliq yo'lni tanlaydi: bir necha xato urinishdan keyingina CAPTCHA so'raydi. Kanon buni qilmadi — bu 2-mashq.
Javob — 400 va Problem Details kod: "captcha", jurnalga bot_tutildi hodisasi (sabab bilan). Hisob yaratilmaydi, xat ketmaydi.
Maslahat: Nega token tanada emas, sarlavhada? Uchala yo'lning tana sxemasi
strictObject— ortiqcha maydon 422 beradi (Backend'dagi injection va mass assignment). Tokenni sarlavhaga qo'yish uchala sxemaga tegmaslikka imkon berdi va tekshiruvni bitta middleware'ga yig'di.
4. Asal idish (honeypot)
4.1 G'oya va "bir xil javob"
Asal idish (honeypot) — formadagi odam ko'rmaydigan maydon (Server Actions xavfsizligi darsida ko'rgansiz). Oddiy spam-bot HTML'dagi hamma maydonni to'ldiradi, odam esa ko'rinmas maydonga hech narsa yozmaydi. vazifalar-api da bu maydon — vebSayt: ro'yxat formasida CSS bilan yashirilgan.
Muhim savol: bot tutilsa, nima javob beramiz? Kanon qarori — aynan o'sha 202 va o'sha matn, odamga beriladigandek. Hisob yaratilmaydi, xat ketmaydi, faqat jurnalga bot_tutildi (usul: honeypot) yoziladi.
Nega 400 emas? Bot muallifi 400 ni ko'rsa, maydonni topadi va keyingi versiyada uni bo'sh qoldiradi. 202 ni ko'rsa — "ishladi" deb o'ylaydi va davom etadi, natijada esa hech narsa yo'q. Bu Ro'yxat va login oqimi dagi "bir xil javob" tamoyilining davomi: tashqaridan qarab, ichkarida nima bo'lganini bilib bo'lmasin.
4.2 Sxemadagi maydon
RoyxatSxemasi — strictObject. Agar vebSayt sxemada bo'lmasa, uni to'ldirgan bot 422 olardi va asal idish o'zini oshkor qilardi. Shuning uchun maydon sxemaga qo'shildi — ixtiyoriy va uzunligi cheklangan:
vebSayt: z.string().max(200).optional(),Asal idishning chegarasi ham bor: u faqat "hamma maydonni to'ldiradigan" sodda botlarni ushlaydi. Saytingiz uchun maxsus yozilgan bot uni chetlab o'tadi. Shuning uchun u CAPTCHA va tezlik cheklovining o'rnida emas, yonida.
5. SMS pumping
5.1 Hujum
OTP orqali login darsida SMS kod yuborishni qo'shgansiz. Har SMS uchun SMS provayderi sizdan pul oladi. Xalqaro raqamlarga — ancha qimmat.
SMS pumping (yoki SMS toll fraud) shunday ishlaydi: hujumchi qimmat tarifli xalqaro raqamlar bilan "hamkorlik" qiladi — bu raqamlarga kelgan har SMS uchun aloqa operatori unga ulush to'laydi. Keyin u skript bilan sizning «Kod yuborish» formangizga shu raqamlarni minglab yuboradi. Sizning hisobingizdan pul ketadi, hujumchi daromad oladi. Hech qanday hisob buzilmaydi, hech qanday ma'lumot o'g'irlanmaydi — faqat to'lov hisobi bir kechada o'sib ketadi.
5.2 To'rt qatlam
| Qatlam | Qayerda qo'shilgan | Nimaga qarshi |
|---|---|---|
| SMS faqat hisobga ulangan / ulanayotgan raqamga | OTP darsi | begona raqamga yuborish |
mamlakat ro'yxati (OTP_MAMLAKATLAR=+998) |
OTP darsi | qimmat xalqaro raqamlar |
| raqam va IP chastotasi | OTP, tezlik cheklovi | bitta manbadan ko'p |
| kunlik global byudjet | bu dars | ko'p IP, ko'p hisob |
Birinchi uchtasi allaqachon bor edi. Ular "bitta hujumchi"ga qarshi. Lekin minglab IP va minglab hisob bilan har biri o'z chegarasi ichida qoladi. Oxirgi chiziq — butun ilova bo'yicha: bir kunda ko'pi bilan SMS_KUNLIK_MAKS ta SMS (sukut 1 000). Oshsa — SMS yuborilmaydi va jurnalga kunlik-byudjet yoziladi.
Byudjet Rate limiting dagi cheklagichdan foydalanadi, faqat kalit — hammaga bitta: sms-kunlik:hammasi, oyna — 24 soat. Yangi jadval ham, yangi kod ham kerak bo'lmadi.
5.3 Byudjet tugasa — signal
Kunlik byudjetga yetish — oddiy holat emas. Bu yo hujum, yo kutilmagan o'sish. Ikkalasida ham odam bilishi kerak. Shuning uchun kanon bu hodisani jurnalga alohida belgi bilan yozadi — monitoring shu belgiga ogohlantirish qo'yadi (OWASP Top 10:2025 xaritasi dagi A09 — "Alerting").
Bir trade-off'ni ochiq aytamiz: byudjet tugagach, haqiqiy foydalanuvchilar ham SMS ololmaydi — kun oxirigacha. Bu ham DoS'ning bir turi. Kanon pulni himoya qilishni tanladi. Katta tizimlarda byudjet mamlakat va prefiks bo'yicha bo'linadi yoki SMS provayderining o'z "fraud guard" xizmati ishlatiladi — kanonda sinalmagan.
Tekshirib ko'ring: Byudjet tekshiruvi nega javobdan keyin, fon ishida turadi? Javobda "byudjet tugadi" deyish yaxshi emasmi?
Javob
OTP so'rovi hamisha bir xil javob qaytaradi — raqam bormi, kod ketdimi, oshkor qilinmaydi (OTP orqali login dagi qoida). "Byudjet tugadi" deyish hujumchiga signal bo'lardi: "hujum ishladi, ertaga davom et" yoki "chegarangiz shu yerda". Javob o'zgarmaydi — farqni faqat jurnal ko'radi.
6. Boshqa CAPTCHA'lar
Kanon faqat Turnstile'ni sinadi. Qolgan ikki keng tarqalgani — hujjat bo'yicha (sinalmagan):
- hCaptcha —
https://api.hcaptcha.com/siteverify, javob shakli o'xshash (success,hostname,error-codes). - reCAPTCHA v3 — vidjetsiz, har so'rovga ball beradi (0.0 — bot, 1.0 — odam). Server
successdan tashqariscoreni o'z chegarasi bilan vaactionnomini solishtiradi. Ball — ehtimollik: chegarani qayerga qo'yishni sizning trafikingiz hal qiladi.
Interfeys tufayli vazifalar-api da provayder almashtirish — bitta yangi klass.
CAPTCHA'ning narxi ham bor: ko'rish yoki eshitish qiyinligi bo'lgan foydalanuvchilarga (masalan, ekran o'quvchidan foydalanadigan Malika) murakkab topshiriqlar to'siq bo'ladi. Turnstile ko'pincha ko'rinmas tekshiruv bilan ishlaydi — lekin baribir yuklanmasa, odam ham o'ta olmaydi. Shuning uchun CAPTCHA faqat kerakli yo'llarda.
7. Hujumchi nigohi
| Hujumchi sinaydi | vazifalar-api |
|---|---|
curl bilan /royxat, X-Captcha siz |
400 captcha, hisob yo'q |
| eski yoki boshqa saytdagi token | timeout-or-duplicate / hostname — 400 |
| Cloudflare'ga yo'lni bloklash (tarmoq xatosi) | rad — yopiq yiqilish |
| formaning hamma maydonini to'ldirish | 202, lekin hisob ham, xat ham yo'q |
| 10 000 IP'dan SMS so'rovi | kunlik byudjet, jurnal signali |
| login'ga CAPTCHA'siz toshqin | tezlik cheklovi va semafor |
8. Ko'p uchraydigan xatolar
| Xato | Nega noto'g'ri | To'g'risi |
|---|---|---|
| tokenni faqat frontendda tekshirish | bot formani ochmaydi | serverda siteverify |
r.ok ga qarab qaror |
rad ham HTTP 200 | success === true |
| tarmoq xatosida o'tkazish | bot shu paytni kutadi | yopiq yiqilish (xavfli yo'llarda) |
| maxfiy kalit frontend kodida | hamma ko'radi | faqat server .env |
test kaliti + hostname tekshiruvi |
hamma rad | test kalitida hostname bermang |
| asal idishda 400 qaytarish | bot maydonni topadi | bir xil 202 |
asal idish maydoni sxemada yo'q (strictObject) |
422 — oshkor | sxemaga ixtiyoriy maydon |
| SMS'ni faqat IP bo'yicha cheklash | ko'p IP | + global kunlik byudjet |
9. Mashqlar
Mashqlar kurs/mashqlar/28/30-captcha/ da.
1-mashq (oson): qaysi javob?
vazifalar-api (CAPTCHA yoqilgan) quyidagilarga nima javob beradi?
POST /royxat,X-Captchayo'q.POST /kirish,X-Captchayo'q, parol to'g'ri, email tasdiqlangan.POST /royxat, token to'g'ri,vebSayt: "https://spam.uz".POST /otp/yubor, token to'g'ri, lekin bugun 1 000 SMS allaqachon ketgan.
Yechim
- 400
captcha, hisob yaratilmaydi, jurnaldabot_tutildi(usul: captcha). - 200 — login CAPTCHA'siz; uni tezlik cheklovi himoyalaydi.
- 202 — odamga beriladigan javobning aynan o'zi. Lekin hisob ham, xat ham yo'q; jurnalda
bot_tutildi(usul: honeypot). - Odatdagi bir xil javob — lekin SMS ketmaydi, jurnalda
kunlik-byudjet.
2-mashq (o'rta): xatolardan keyin CAPTCHA
Login uchun "aqlli" qoida yozing: captchaKerakmi(xatolar) — oxirgi 15 daqiqadagi xato urinishlar soni 3 va undan ko'p bo'lsa true. Keyin kirish(holat, captchaOk) funksiyasi qaror qaytarsin: CAPTCHA kerak, lekin captchaOk emas — "400 captcha"; aks holda — "tekshirish". Besh holatni sinang: 0, 2, 3 xato (CAPTCHA'siz) va 3, 5 xato (CAPTCHA bilan).
Yechim
const CHEGARA = 3;
const captchaKerakmi = (xatolar) => xatolar >= CHEGARA;
function kirish(xatolar, captchaOk) {
if (captchaKerakmi(xatolar) && !captchaOk) return "400 captcha";
return "tekshirish";
}
for (const [xatolar, ok] of [[0, false], [2, false], [3, false],
[3, true], [5, true]]) {
console.log(`xato ${xatolar}, captcha ${ok}:`, kirish(xatolar, ok));
}Konsolda:
xato 0, captcha false: tekshirish
xato 2, captcha false: tekshirish
xato 3, captcha false: 400 captcha
xato 3, captcha true: tekshirish
xato 5, captcha true: tekshirishHalol odam deyarli hech qachon CAPTCHA ko'rmaydi, parol terayotgan bot esa uchinchi urinishdan keyin to'siladi. Xato hisobi qaysi kalit bo'yicha — hisob, IP yoki ikkalasi — tezlik cheklovidagi kabi qaror. Faqat hisob bo'yicha bo'lsa, hujumchi qurbonga CAPTCHA "yuklab" qo'yishi mumkin; bu, ehtimol, qabul qilinadigan narx.
3-mashq (qiyin): kunlik byudjet va prefiks
SmsByudjet klassini yozing: umumiy kunlik chegara va har mamlakat prefiksi uchun alohida chegara. ruxsat(telefon) — ikkala chegara ichida bo'lsa hisoblagichlarni oshirib true, aks holda false qaytarsin (hisoblagich oshirilmaydi). Prefiks — ro'yxatdagi eng uzun mos keluvchisi (+998, +7); ro'yxatda yo'q raqam — false. Sinov: umumiy 4, +998 uchun 3, +7 uchun 1.
Yechim
class SmsByudjet {
#jami = 0;
#prefikslar = new Map();
constructor(umumiy, chegaralar) {
this.umumiy = umumiy;
this.chegaralar = chegaralar;
}
#prefiks(tel) {
return Object.keys(this.chegaralar)
.filter((p) => tel.startsWith(p))
.toSorted((a, b) => b.length - a.length)[0];
}
ruxsat(tel) {
const p = this.#prefiks(tel);
if (p === undefined) return false;
const n = this.#prefikslar.get(p) ?? 0;
if (this.#jami >= this.umumiy || n >= this.chegaralar[p]) {
return false;
}
this.#jami++;
this.#prefikslar.set(p, n + 1);
return true;
}
}
const b = new SmsByudjet(4, { "+998": 3, "+7": 1 });
for (const t of ["+998901", "+79001", "+79002", "+998902",
"+998903", "+998904", "+44700"]) {
console.log(t, b.ruxsat(t));
}Konsolda:
+998901 true
+79001 true
+79002 false
+998902 true
+998903 true
+998904 false
+44700 false+79002 — +7 chegarasi (1) tugadi. +998904 — umumiy chegara (4) tugadi, garchi +998 uchun ham joy qolmagan bo'lsa ham. +44700 — prefiks ro'yxatda yo'q. Real kodda hisoblagich baza yoki Redis'da va 24 soatlik oynada turadi — bu yerda faqat qaror mantig'i.
10. Loyiha qadami: captcha.ts, asal idish va SMS byudjeti
10.1 src/auth/captcha.ts
Yangi modul — to'liq. Telefon uchun bo'lingan, boshidagi izoh qisqartirilgan; mantiq kanondagidek (bo'lingan nusxani kanon loyihasi nusxasida tsc, ESLint va testlar bilan tekshirdik):
// captcha.ts — CAPTCHA tokenini SERVERDA tekshirish: Cloudflare
// Turnstile siteverify (developers.cloudflare.com/turnstile). Brauzer
// vidjeti token beradi; token o'zi hech narsani isbotlamaydi — server
// uni sir kalit bilan Cloudflare'ga yuborib tekshiradi (bir marta,
// 300 s). Kanon sinovi: fetch — tikuv (mock); haqiqiy siteverify —
// faqat RASMIY TEST kalitlari bilan, haqiqiy hisob ishlatilmagan
export const SITEVERIFY =
"https://challenges.cloudflare.com/turnstile/v0/siteverify";
export interface CaptchaTekshiruvchi {
/** true — inson (yoki test kaliti); tarmoq xatosi va javobsizlik
* — false (yopiq holatda xavfsiz) */
tekshir(
token: string | undefined,
ip: string | undefined,
): Promise<{ ok: boolean; sabab?: string }>;
}
type Javob = {
success?: unknown;
"error-codes"?: unknown;
hostname?: unknown;
action?: unknown;
};
export class Turnstile implements CaptchaTekshiruvchi {
readonly #sir: string;
readonly #fetch: typeof fetch;
readonly #hostname: string | undefined;
/** @param hostname berilsa — token boshqa saytda olingan bo'lsa
* rad (token "ko'chirib" kelinmasin) */
constructor(
sir: string,
{ fetchFn = globalThis.fetch, hostname }:
{ fetchFn?: typeof fetch; hostname?: string } = {},
) {
this.#sir = sir;
this.#fetch = fetchFn;
this.#hostname = hostname;
}
async tekshir(
token: string | undefined,
ip: string | undefined,
): Promise<{ ok: boolean; sabab?: string }> {
if (token === undefined || token === "" || token.length > 2048) {
return { ok: false, sabab: "token-yoq" };
}
const tana = new URLSearchParams({
secret: this.#sir,
response: token,
...(ip === undefined ? {} : { remoteip: ip }),
});
let j: Javob;
try {
const r = await this.#fetch(SITEVERIFY, {
method: "POST",
body: tana,
signal: AbortSignal.timeout(3000),
});
j = (await r.json()) as Javob;
} catch {
return { ok: false, sabab: "siteverify-xato" };
}
if (j.success !== true) {
const kodlar = j["error-codes"];
const sabab = Array.isArray(kodlar) ? kodlar.join(",") : "rad";
return { ok: false, sabab };
}
const kutilgan = this.#hostname;
if (kutilgan !== undefined && j.hostname !== kutilgan) {
return { ok: false, sabab: "hostname" };
}
return { ok: true };
}
}try ichida ikki narsa bor: fetch ham, r.json() ham. Cloudflare JSON o'rniga HTML xato sahifasi qaytarsa, json() xato tashlaydi — u ham siteverify-xato va rad.
10.2 Marshrut, sxema va byudjet
src/auth/marshrut.ts — CAPTCHA middleware va asal idish (bo'lingan):
// CAPTCHA — bot hujumi nishoni bo'lgan yo'llar (hisob ochish,
// xat/SMS yuborish). Token sarlavhada: X-Captcha (vidjet bergan)
const captchaKerak = async (req: Request): Promise<void> => {
if (d.captcha === undefined) return;
const n = await d.captcha.tekshir(req.get("x-captcha"), req.ip);
if (!n.ok) {
await xizmat.hodisa({
tur: "bot_tutildi", ...mijoz(req),
tafsilot: { usul: "captcha", sabab: n.sabab },
});
throw new IlovaXatosi(400,
"CAPTCHA tasdiqlanmadi — sahifani yangilab " +
"qayta urinib ko'ring",
{ kod: "captcha", sarlavha: "CAPTCHA tasdiqlanmadi" });
}
};
for (const y of ["/royxat", "/parol-tiklash/sorov", "/otp/yubor"]) {
r.use(y, (req, _res, next) => {
if (req.method !== "POST") { next(); return; }
captchaKerak(req).then(() => { next(); }, next);
});
}captchaKerak(req).then(() => { next(); }, next) — Express 5 middleware'ida asinxron xato next(xato) orqali markaziy xato ishlovchiga boradi. U 400 ni Problem Details'ga aylantiradi.
/royxat ishlovchisining boshida — asal idish:
// honeypot: bot yashirin maydonni to'ldirdi — AYNAN o'sha 202
// (bot "muvaffaqiyat" ko'rsin); hisob ham, xat ham yo'q — jurnal
if (body.vebSayt !== undefined && body.vebSayt !== "") {
await xizmat.hodisa({
tur: "bot_tutildi", ...mijoz(req),
tafsilot: { usul: "honeypot" },
});
res.status(202).json({ xabar: XAT_YUBORILDI });
return;
}src/auth/sxema.ts da RoyxatSxemasi ga vebSayt: z.string().max(200).optional() qo'shildi. src/auth/ombor.ts dagi hodisa turlariga — "bot_tutildi".
src/auth/xizmat.ts — SMS fon ishining boshida, kod yaratishdan oldin:
// kunlik global byudjet — raqam/IP cheklovlaridan tashqari
// (ko'p IP, ko'p hisob bilan "nasos"); oshsa — SMS yo'q,
// jurnalga (ogohlantirish signali)
const b = await d.cheklagich.ol("sms-kunlik:hammasi",
d.smsKunlikMaks ?? 1000, 86_400_000);
if (!b.ruxsat) {
await ombor.hodisa({
tur: "otp_sorov", foydalanuvchiId: egasi, ...mijoz,
tafsilot: { maqsad, cheklov: "kunlik-byudjet" },
});
return;
}86_400_000 — 24 soat millisekundda. Byudjet faqat SMS haqiqatan yuboriladigan holatda sarflanadi (raqam topilgandan keyin) — begona raqamga so'rovlar byudjetni "yemaydi".
Sozlamalar (src/config.ts, Zod bilan): TURNSTILE_SIRI (ixtiyoriy, kamida 10 belgi; berilmasa CAPTCHA o'chiq), TURNSTILE_HOSTNAME (ixtiyoriy), SMS_KUNLIK_MAKS (sukut 1 000). src/server.ts ularni ulaydi:
captcha: config.TURNSTILE_SIRI === undefined ? undefined
: new Turnstile(config.TURNSTILE_SIRI,
config.TURNSTILE_HOSTNAME === undefined
? {} : { hostname: config.TURNSTILE_HOSTNAME }),
smsKunlikMaks: config.SMS_KUNLIK_MAKS,10.3 Testlar
test/bot.test.ts — 4 test. Birinchisi Cloudflare'siz (soxta fetch): so'rov tanasi, error-codes, hostname, tarmoq xatosi, tokensiz holat (fetch umuman chaqirilmaydi). Qolgan uchtasi haqiqiy Postgres bilan: CAPTCHA'li API, asal idish va byudjet. Asal idish testining asosi (bo'lingan):
const a = await request(app).post("/v1/auth/royxat").send({
email: "bot@example.com", toliqIsm: "Bot", parol: PAROL,
vebSayt: "https://spam.example",
}).expect(202);
const b = await request(app).post("/v1/auth/royxat").send({
email: "odam@example.com", toliqIsm: "Odam", parol: PAROL,
vebSayt: "",
}).expect(202);
assert.deepEqual(a.body, b.body);
await d.fon.tugashi();
assert.deepEqual((await pool.query<{ email: string }>(
"SELECT email FROM foydalanuvchilar")).rows,
[{ email: "odam@example.com" }]);
assert.deepEqual(d.pochtachi.yuborilgan.map((x) => x.kimga),
["odam@example.com"]);assert.deepEqual(a.body, b.body) — testning yuragi: bot va odam bir xil javob oldi. Farq faqat bazada va pochtada.
Ishga tushiramiz (biz ishga tushirdik: kanon nusxasi, bo'lingan captcha.ts va marshrut qismlari, Node 24.21.0, test bazasi — lokal PostgreSQL 17.6 klasterida kanon sxemasi). Har test nomi bitta himoya qatlamini aytadi:
node --test test/bot.test.ts▶ Turnstile (mock fetch)
✔ success — ok; error-codes — rad (sabab bilan); hostname mos emas — rad; tarmoq xatosi — rad (yopiq holat); tokensiz — fetch chaqirilmaydi (7.0021ms)
✔ Turnstile (mock fetch) (8.2728ms)
✔ CAPTCHA yoqilgan: X-Captcha'siz yoki noto'g'ri — 400 captcha (hisob yaratilmaydi); to'g'ri — 202; login — CAPTCHA'siz (1047.8054ms)
✔ honeypot: yashirin vebSayt maydoni to'ldirilgan — o'sha 202, lekin hisob ham, xat ham yo'q (407.2057ms)
✔ SMS pumping: kunlik global byudjet (testda 1) — ikkinchi foydalanuvchiga SMS ketmaydi, jurnalda kunlik-byudjet (457.25ms)
ℹ tests 4
ℹ suites 1
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 29376.4718Nima bo'ldi? To'rtta test — to'rtta qatlam: token tekshiruvi, CAPTCHA'li yo'llar, asal idish va kunlik byudjet. Postgres yo'q bo'lsa, oxirgi uchtasi "skipped" bo'ladi — Turnstile testi baribir ishlaydi. Testlarda Cloudflare'ga birorta so'rov ketmaydi.
10.4 Tekshiruv va commit
Butun loyiha tekshiruvi (kanon, 2026-10-28, PostgreSQL 18.6): tip va lint xatosiz, testlar 233 (232 + 1 o'tkazilgan) — to'rtta yangi test; brauzer testlari 10/10, migratsiya yo'q.
.env.example ni bu darsda o'zgartirmang — keyingi darsda butun 28-qism sozlamalari bitta bo'limga yig'iladi (Turnstile test kaliti tuzog'i haqidagi izoh bilan).
M="feat(auth): bot himoyasi — Turnstile siteverify (X-Captcha),"
M+=" honeypot maydoni, SMS kunlik global byudjeti"
git add src/auth src/config.ts src/server.ts test/bot.test.ts
git commit -m "$M"11. Real ishda
- Cloudflare Turnstile, hCaptcha, reCAPTCHA — eng keng tarqalgan uchtasi; hammasida server tomoni
siteverifyga o'xshash bittaPOST. - SMS provayderlari (Twilio va boshqalar) SMS pumping'ni alohida xavf sifatida hujjatlaydi va mamlakat ro'yxati, chegaralar va "fraud guard" xizmatlarini taklif qiladi. O'zbekistondagi provayderlarda ham kunlik limit sozlamasi bo'lishi mumkin — shartnomadan tekshiring.
- Bot boshqaruvi katta saytlarda alohida mahsulot: xulq tahlili, qurilma izi, IP obro'si. CAPTCHA — uning faqat bir qismi.
- Intervyuda: "CAPTCHA tokenini qanday tekshirasiz?", "Honeypot nima va nega 200 qaytaradi?", "SMS pumping nima?", "Fail open va fail closed qachon?".
Xulosa
- CAPTCHA tokeni faqat serverda
siteverifyorqali tasdiqlanadi; qaror —success === truebo'yicha, HTTP status bo'yicha emas. - Rasmiy test kaliti
hostname: "example.com"qaytaradi — domen tekshiruvi bilan birga ishlatib bo'lmaydi. - Tarmoq xatosida CAPTCHA — yopiq yiqilish; yo'lni tanlash — "rad etish" va "o'tkazish" narxini solishtirish.
- Asal idish: maydon sxemada, bot odam bilan aynan bir xil 202 oladi, farq faqat jurnalda.
- SMS pumping — pulga qarshi hujum; to'rt qatlam, oxirgisi — kunlik global byudjet va jurnal signali.
Keyingi dars: OWASP API Security Top 10 — qism yakuni: API1–API10 ro'yxati bo'yicha vazifalar-api auditi, 28-qismda topilgan haqiqiy zaifliklar jadvali, regressiya testlari va frontend'ni haqiqiy kirishga ulash.
Manbalar
- Cloudflare Turnstile — Server-side validation: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ (2026-10-28 da o'qildi)
- Cloudflare Turnstile — Testing (test kalitlari): https://developers.cloudflare.com/turnstile/troubleshooting/testing/
- hCaptcha — Verify the user response server side: https://docs.hcaptcha.com/#verify-the-user-response-server-side
- Google reCAPTCHA v3: https://developers.google.com/recaptcha/docs/v3
- OWASP API Security Top 10:2023 — API6 Unrestricted Access to Sensitive Business Flows: https://owasp.org/API-Security/editions/2023/en/0xa6-unrestricted-access-to-sensitive-business-flows/
- Twilio — SMS pumping fraud: https://www.twilio.com/docs/glossary/what-is-sms-pumping-fraud
- Kanon (IlmHamroh, 2026-10-28): Turnstile sinovi (haqiqiy siteverify, faqat rasmiy test kalitlari, Node 24.21.0),
captcha.ts,bot.test.ts, holat tekshiruvi. Brauzer misollari vabot.test.tsnatijasi — biz ishga tushirdik (2026-10-28, Node 24.21.0, test bazasi PostgreSQL 17.6).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!