Mundarija (34)
- Bu darsda
- 1. Nega bu kerak?
- 2. Sessiya qanday ishlaydi
- 3. express-session: tayyor yechim va uning tuzoqlari
- 3.1 Sukut sozlamalar
- 3.2 To'g'ri sozlama
- 4. Kanon: o'z sessiya ombori
- 4.1 Token va uning hash'i
- 4.2 Nega bu yerda argon2 kerak emas
- 4.3 Bazada sinab ko'ring
- 5. Ikki xil muddat
- 5.1 Idle va absolute
- 5.2 Har so'rovda yozmaslik
- 6. Session fixation
- 7. Store tanlash: Postgres yoki Redis
- 8. Chiqish va "barcha qurilmalardan chiqish"
- 9. Har so'rov — o'z foydalanuvchisi: yashirin zaifliklar ochildi
- 10. Hujumchi nigohi
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): sessiya holati
- 3-mashq (qiyin): xotiradagi sessiya ombori
- 13. Loyiha qadami: sessiyalar
- 13.1 Migratsiya
- 13.2 src/auth/sessiya.ts
- 13.3 src/auth/middleware.ts
- 13.4 Router: sessiya berish va chiqish
- 13.5 Ilova: har so'rov o'z ombori
- 13.6 Testlar
- 13.7 Jonli API va commit
- 14. Real ishda
- Xulosa
- Manbalar
Session-based auth: server sessiyalari, fixation va "barcha qurilmalardan chiqish"
Qisqacha: Sessiyada server foydalanuvchini o'zi eslab qoladi, brauzer esa faqat tasodifiy kalitni — sessiya tokenini — cookie'da saqlaydi. Kanon
express-sessiono'rniga o'z sessiya omborini yozdi: token — 32 tasodifiy bayt, bazada faqat uning SHA-256 hash'i, cookie —__Host-sessiya(HttpOnly,Secure,SameSite=Lax). Chiqish — bazada bittaUPDATE, darhol. Foydalanuvchi paydo bo'lgach, 25-qismdan beri yashirinib yotgan uchta zaiflik ochildi: begonaIdempotency-Keybilan boshqa odamning vazifasini olish, begona eksport ishini o'qish va unutilgan versiyasiz yo'llar.
Bu darsda
- Sessiya qanday ishlashini — login'dan chiqishgacha — qadamma-qadam kuzatasiz.
express-sessionsukut sozlamalarining tuzoqlarini kanon o'lchovida ko'rasiz.- Tokenni nega bazada hash qilib saqlash kerakligini va nega u uchun argon2 shart emasligini tushuntirasiz.
- Idle va mutlaq muddat, session fixation, "barcha qurilmalardan chiqish" ni yozasiz.
vazifalar-apiga sessiyalar jadvali,SessiyaOmbori, middleware va 16 ta testni qo'shasiz; uchta BOLA zaifligini yopasiz.
Oldin bilishingiz kerak: Cookie chuqur, Ro'yxat va login oqimi, Middleware va request lifecycle, Idempotency-Key, String, TTL va kalit nomlash.
Ikki darsdan beri tayyorgarlik ko'rdik: parol xavfsiz saqlanadi, login "bu Aziz" deydi, cookie atributlarini bilamiz. Bugun ularni bitta zanjirga ulaymiz: Aziz bir marta kiradi va keyingi har so'rovda server uni taniydi.
1. Nega bu kerak?
HTTP xotirasiz (stateless): har so'rov alohida, server oldingisini eslamaydi. Login'dan keyin GET /v1/vazifalar kelganda server "bu kim?" deb so'raydi. Ikki asosiy javob bor:
| Yondashuv | Brauzerda nima | Serverda nima |
|---|---|---|
| sessiya (session) | tasodifiy token (ma'nosiz kalit) | "bu token — Aziz, muddati …" yozuvi |
| token (masalan, JWT) | imzolangan "men — Aziz" hujjati | faqat imzo kaliti |
Bugun birinchisini quramiz. Ikkinchisi — keyingi ikki darsda; qaysi biri qachon yaxshiroq — Session vs JWT qarori darsida, o'lchovlar bilan.
Sessiyaning asosiy afzalligi — nazorat: server istalgan payt "bu sessiya endi yo'q" deya oladi. Telefon yo'qoldi — bir tugma, va u telefon endi hech narsa ko'rmaydi.
2. Sessiya qanday ishlaydi
sequenceDiagram
participant B as Brauzer
participant A as vazifalar-api
participant DB as Postgres
B->>A: POST /v1/auth/kirish (email, parol)
A->>DB: argon2 tekshiruvi, keyin sessiya qatori (token hash)
A-->>B: 200 + Set-Cookie __Host-sessiya=token
B->>A: GET /v1/vazifalar (Cookie bilan)
A->>DB: token hash bo'yicha faol sessiya va foydalanuvchi
A-->>B: 200 faqat Azizning vazifalari
B->>A: POST /v1/auth/chiqish
A->>DB: bekor_vaqti = now()
A-->>B: 204 + cookie o'chiriladiBrauzer tokenning ma'nosini bilmaydi — u shunchaki 43 belgili tasodifiy satr. Hamma ma'lumot (kim, qachongacha, qaysi qurilma) serverda. Shuning uchun token sizsa ham, hujumchi undan hech narsa "o'qiy" olmaydi — faqat uni ishlatishi mumkin. Bunga qarshi HttpOnly, Secure va qisqa muddat.
3. express-session: tayyor yechim va uning tuzoqlari
3.1 Sukut sozlamalar
Express olamidagi eng mashhur yechim — express-session paketi va unga store (ombor) adapterlari: connect-pg-simple (Postgres), connect-redis (Redis). Kanon ularni tajribada sinadi (express-session 1.19.0, connect-pg-simple 10.0.0, connect-redis 10.0.0). Birinchi qadam — hujjatdagi eng qisqa misol, faqat store va secret bilan. Natijada ikki narsaga qarang: ogohlantirishlar va oxirgi qatordagi qatorlar soni.
express-session deprecated undefined resave option; provide resave option at file:\C:\Users\…\tajriba.mjs:28:11
express-session deprecated undefined saveUninitialized option; provide saveUninitialized option at file:\C:\Users\…\tajriba.mjs:28:11
anonim GET / → Set-Cookie: connect.sid=s%3A<sid>.<imzo>; Path=/; HttpOnly
100 ta anonim (cookie'siz) GET dan keyin bazada sessiya qatorlari: 100 (saveUninitialized sukut true — har tashrifchiga qator: DoS/bloat)Sana, lokal yo'l va fayl nomini qisqartirdik. Paket ikki ogohlantirish berdi: "resave va saveUninitialized sozlamalari berilmagan". Uchinchi qator — oqibati: login qilmagan 100 ta tashrifchiga 100 ta sessiya qatori yozildi. Har bot, har skaner bazada iz qoldiradi. Bu — bepul DoS: hujumchi millionlab so'rov bilan jadvalni to'ldiradi.
3.2 To'g'ri sozlama
app.use(session({
store,
secret: process.env.SESSIYA_SIRI, // .env da, 32+ bayt
resave: false, // o'zgarmagan sessiyani qayta yozmaslik
saveUninitialized: false, // bo'sh sessiya — qatorsiz
name: "sid", // connect.sid texnologiyani oshkor qiladi
cookie: {
httpOnly: true, secure: true, sameSite: "lax",
maxAge: 30 * 86_400_000,
},
}));
app.post("/kirish", (req, res, next) => {
// ... parol tekshirildi
req.session.regenerate((x) => { // fixation himoyasi
if (x) return next(x);
req.session.foydalanuvchiId = 1;
res.send("kirdi");
});
});Kanon natijasi: 20 ta anonim so'rovdan keyin bazada 0 qator. Cookie esa shunday: sid=s%3A<sid>.<HMAC> — oldingi darsdagi imzolangan cookie (s: prefiksi). Va eng muhim kuzatuv:
store kaliti (sid) cookie ichidagi bilan bir xil: trueYa'ni bazada sessiya kaliti — cookie'dagi ID ning o'zi, ochiq holda. Baza sizib chiqsa, hujumchi har sessiya ID'sini ko'radi. Imzo uni to'xtatadi, lekin faqat secret sizmaguncha. Kod va .env ham sizsa (bu ko'pincha birga bo'ladi) — istalgan foydalanuvchi sessiyasini o'g'irlash mumkin.
4. Kanon: o'z sessiya ombori
4.1 Token va uning hash'i
Kanon boshqa yo'lni tanladi. Sessiya tokeni — 32 tasodifiy bayt (256 bit), cookie'da base64url ko'rinishida. Bazada esa token emas, uning SHA-256 hash'i:
const enc = new TextEncoder();
const b64url = (b) => btoa(String.fromCharCode(...b))
.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
const hex = (buf) => Array.from(new Uint8Array(buf),
(x) => x.toString(16).padStart(2, "0")).join("");
// 1) yangi token — 32 tasodifiy bayt (har safar boshqa)
const token = b64url(crypto.getRandomValues(new Uint8Array(32)));
console.log(token.length, /^[A-Za-z0-9_-]{43}$/.test(token));
// 2) bazaga faqat hash ketadi — bir xil token, bir xil hash
const h1 = await crypto.subtle.digest(
"SHA-256", enc.encode("token-A"));
const h2 = await crypto.subtle.digest(
"SHA-256", enc.encode("token-A"));
console.log(h1.byteLength, hex(h1) === hex(h2));
console.log(hex(h1).slice(0, 16));Konsolda:
43 true
32 true
ee718602fc388f86Token har safar boshqa, shuning uchun uning o'zini emas, uzunligi va shaklini chiqardik: 32 bayt base64url'da doim 43 belgi. Bu shakl muhim — kanon cookie'dagi qiymatni avval shu regex bilan tekshiradi, boshqa shakldagi satr bazaga ham bormaydi.
Baza sizsa ham, hujumchi ee7186… dan token-A ni tiklay olmaydi — SHA-256 bir tomonlama.
4.2 Nega bu yerda argon2 kerak emas
Oldingi darslarda "tez hash — parol uchun halokat" dedik. Endi esa SHA-256? Farq — entropiyada:
- Parolni odam o'ylaydi:
Toshkent2026. Variantlar kam, hujumchi mashhurlarini sinaydi. Sekin hash har urinishni qimmatlashtiradi. - Tokenni kompyuter yaratadi: 2^256 variant. Hatto sekundiga milliard SHA-256 bilan ham uni terib topish koinot yoshidan uzoq.
Yana bir sabab: tekshirishda server tokenni qidiradi. Tuzli hash bilan qidirib bo'lmaydi — har qatorning tuzi boshqa. Tuzsiz SHA-256 esa deterministik: WHERE token_hash = $1 va noyob indeks. Xulosa: tuz va sekin hash — inson o'ylagan sirlar uchun; tasodifiy uzun token uchun oddiy SHA-256 yetarli.
4.3 Bazada sinab ko'ring
Kanonning sql/112-sessiyalar.sql migratsiyasi (to'liq matni loyiha qadamida) qo'llangan lokal bazada (biz ishga tushirdik, PostgreSQL 17.6). Postgres'ning sha256() funksiyasi JS'dagi bilan bir xil hash beradi:
INSERT INTO sessiyalar (foydalanuvchi_id, token_hash, tugash_vaqti)
VALUES (1, sha256('token-A'::bytea), now() + interval '30 days')
RETURNING id, octet_length(token_hash) AS bayt;Natija:
id | bayt
----+------
1 | 32
(1 row)
INSERT 0 1Noto'g'ri uzunlikdagi "hash" (16 baytli MD5) — rad:
\set VERBOSITY terse
INSERT INTO sessiyalar (foydalanuvchi_id, token_hash, tugash_vaqti)
VALUES (1, decode(md5('token-B'), 'hex'), now() + interval '1 day');Natija:
ERROR: new row for relation "sessiyalar" violates check constraint "sessiyalar_token_hash_check"CHECK (octet_length(token_hash) = 32) — bazada faqat SHA-256 hajmidagi qiymat. Kimdir kodda xato qilib tokenning o'zini (43 bayt) yozmoqchi bo'lsa, baza qabul qilmaydi.
Endi kanon tekshir() so'rovining soddalashtirilgan nusxasi:
SELECT s.id, f.email
FROM sessiyalar s JOIN foydalanuvchilar f ON f.id = s.foydalanuvchi_id
WHERE s.token_hash = sha256('token-A'::bytea)
AND s.bekor_vaqti IS NULL
AND s.tugash_vaqti > now()
AND s.oxirgi_faollik > now() - interval '7 days';Natija:
id | email
----+------------------
1 | aziz@example.com
(1 row)Bitta so'rov uchta savolga javob beradi: token bormi, bekor qilinmaganmi, muddati o'tmaganmi — va shu bilan birga foydalanuvchini ham JOIN bilan olib keladi.
5. Ikki xil muddat
5.1 Idle va absolute
Sessiyaning ikki "soati" bor:
- Idle (bo'sh turish) muddati — oxirgi so'rovdan beri qancha vaqt o'tdi. Kanonda 7 kun. Aziz bir hafta kirmasa — qaytadan login.
- Absolute (mutlaq) muddat — sessiya yaratilganidan beri. Kanonda 30 kun. Aziz har kuni kirsa ham, 30 kundan keyin parol so'raladi.
Nega ikkalasi? Faqat idle bo'lsa, o'g'irlangan token hujumchi har kuni ishlatib tursa abadiy yashaydi. Faqat absolute bo'lsa, kutubxonadagi kompyuterda qolgan sessiya 30 kun ochiq turadi.
Idle'ni sinaymiz — sessiyani 8 kun jim turgandek qilamiz:
UPDATE sessiyalar SET oxirgi_faollik = now() - interval '8 days'
WHERE id = 1;Natija:
UPDATE 1SELECT count(*) AS faol
FROM sessiyalar s
WHERE s.token_hash = sha256('token-A'::bytea)
AND s.bekor_vaqti IS NULL
AND s.tugash_vaqti > now()
AND s.oxirgi_faollik > now() - interval '7 days';Natija:
faol
------
0
(1 row)Qator bazada turibdi, lekin so'rov uni "ko'rmaydi" — sessiya tugagan.
5.2 Har so'rovda yozmaslik
Idle hisoblash uchun oxirgi_faollik ni yangilab turish kerak. Har so'rovda UPDATE — har o'qish so'rovi yozuvga aylanadi. Kanon murosaga keldi: oxirgi_faollik faqat 60 soniyadan eski bo'lsa yangilanadi. 7 kunlik chegara uchun bir daqiqa xato — sezilmaydi, yozuvlar esa keskin kamayadi. Bu "rolling" (siljiydigan) sessiyaning arzon varianti: faol foydalanuvchining idle soati doim oldinga suriladi.
6. Session fixation
Hujum ssenariysi: hujumchi saytga kiradi va o'zining sessiya tokenini oladi. Keyin uni qandaydir yo'l bilan qurbonning brauzeriga qo'yadi (masalan, bir saytdagi boshqa subdomen orqali — oldingi darsdagi Domain muammosi). Qurbon login qiladi. Agar server login'da o'sha tokenni "Aziz" deb belgilasa — hujumchi ham endi Aziz: token uning qo'lida.
Himoya oddiy: login paytida har doim yangi token beriladi, eskisi (kelgan bo'lsa) bekor qilinadi. express-session da bu req.session.regenerate(). Kanon testi ssenariyni to'liq o'ynaydi (telefon uchun bo'lingan):
test("session fixation: hujumchi qo'ygan (o'zining haqiqiy) " +
"cookie bilan qurbon kirsa — YANGI token, eski bekor", async () => {
await royxat("aziz@example.com");
const hujumchi = await royxat("hujumchi@example.com");
const r = await request(app).post("/v1/auth/kirish")
.set("Cookie", hujumchi)
.send({ email: "aziz@example.com", parol: PAROL }).expect(200);
const yangi = cookieniOl(r.headers["set-cookie"]);
assert.notEqual(yangi, hujumchi);
// hujumchining cookie'si endi hech narsaga yaramaydi
await request(app).get("/v1/auth/men")
.set("Cookie", hujumchi).expect(401);
const men = await request(app).get("/v1/auth/men")
.set("Cookie", yangi).expect(200);
assert.equal(
(men.body as { foydalanuvchi: { email: string } })
.foydalanuvchi.email,
"aziz@example.com");
});7. Store tanlash: Postgres yoki Redis
Kanon sessiyani tekshirish narxini o'lchadi — bitta sessiya, 1000 marta ketma-ket, mediana:
- connect-pg-simple1,163 ms
- connect-redis0,872 ms
- kanon tekshir (JOIN)1,605 ms
- kanon: noto'g'ri token1,302 ms
- kanon: noto'g'ri shakl0 ms
Manba: Kanon o'lchovi (IlmHamroh, 2026-10): Node 24.21.0, PostgreSQL 18.6, Redis 8.10.2, express-session 1.19.0, i5-12500H, 2026-10-27
Redis eng tez — 0.87 ms. Kanon tekshiruvi eng sekini — 1.6 ms, chunki u sessiya bilan birga foydalanuvchini ham JOIN bilan oladi (store'lar faqat sessiya obyektini qaytaradi, foydalanuvchi uchun yana bitta so'rov kerak bo'lardi). Noto'g'ri shakldagi token — 0.000 ms: regex uni bazaga yetkazmaydi. 100 ta parallel tekshiruv (ulanishlar hovuzi — pool — 5 ta ulanish) jami 52.1 ms.
Kanon qarori — Postgres. Sabablar:
- 0.3–0.7 ms farq HTTP so'rovning umumiy vaqti ichida sezilmaydi.
- Sessiya yo'qolmasin. Redis xotira to'lganda kalitlarni o'chirishi mumkin (eviction, Redis persistence va operatsiya) — bu foydalanuvchilarning tasodifiy chiqib ketishi.
- "Barcha qurilmalardan chiqish" va qurilmalar ro'yxati — oddiy SQL (
WHERE foydalanuvchi_id = $1). - Bitta infratuzilma: Redis loyihada kesh uchun ixtiyoriy, auth unga bog'lanmasin.
Juda katta yuklamada (sekundiga minglab so'rov) Redis ustunlik qilishi mumkin — u holda qaror o'lchov bilan qayta ko'riladi.
8. Chiqish va "barcha qurilmalardan chiqish"
POST /v1/auth/chiqish— joriy sessiya bazada bekor (bekor_vaqti = now()), cookie o'chiriladi (Expires=1970). Sessiya bo'lmasa ham 204 — chiqish idempotent: ikki marta bosilsa, xato yo'q.POST /v1/auth/chiqish-hammasi— shu foydalanuvchining hamma faol sessiyalari bittaUPDATEbilan. Telefon yo'qolsa yoki parol sizsa — kerak bo'ladigan tugma.
Bazada sinaymiz — Azizga uchta qurilmadan yangi sessiya ochamiz va hammasini yopamiz:
INSERT INTO sessiyalar (foydalanuvchi_id, token_hash, tugash_vaqti)
SELECT 1, sha256(('qurilma-' || i)::bytea), now() + interval '30 days'
FROM generate_series(1, 3) AS i;Natija:
INSERT 0 3UPDATE sessiyalar SET bekor_vaqti = now()
WHERE foydalanuvchi_id = 1 AND bekor_vaqti IS NULL;Natija:
UPDATE 4To'rtta: uchta yangi va idle tufayli tugagan birinchisi (u hali bekor qilinmagan edi). Qatorlar o'chirilmaydi — bekor_vaqti qoladi, keyinroq "qachon, qaysi qurilmadan chiqildi" degan savolga javob beradi.
JWT bilan solishtiring: imzolangan token o'z muddatigacha amal qiladi va uni "o'chirib" bo'lmaydi. Sessiyada chiqish — darhol. Bu farq keyingi darslarda hal qiluvchi bo'ladi.
9. Har so'rov — o'z foydalanuvchisi: yashirin zaifliklar ochildi
25–27-qismlarda API bitta foydalanuvchi nomidan ishlardi: EGASI_ID=1 sozlamasi. Endi har so'rovning egasi — sessiyadagi foydalanuvchi. Kanon buni ulaganda ikkinchi foydalanuvchi (Dilnoza) bilan testlar yozdi — va 25-qismdan beri bizning kodimizda turgan zaifliklar ochildi.
1. Idempotency-Key umumiy edi (BOLA). Idempotency-Key darsidagi middleware javoblarni faqat kalit bo'yicha saqlardi. Kanon testi: Aziz kalit-123 bilan vazifa yaratdi. Dilnoza o'sha kalit va o'sha tana bilan POST yubordi — va Idempotency-Replayed: true bilan Azizning vazifasini oldi. Test yiqildi. Bu — BOLA (Broken Object Level Authorization): obyekt egasi tekshirilmagan. Tuzatish — ichki kalitga egasini qo'shish: "<egasi>:<kalit>".
2. Eksport ishlari egasiz edi. Eksport ishlari xotiradagi umumiy Map da UUID bilan saqlanardi. ID'ni bilgan har kim boshqa odamning eksport faylini yuklab olardi. Tuzatish — ishga egasi maydoni; begona ish — 404, 403 emas: "bunday ish bor, lekin sizniki emas" deyish ham ma'lumot oshkor qiladi.
3. Yashirin versiyasiz yo'llar (API9). API versiyalash darsida eski yo'llar uchun app.use(eskirgan(…), v1) yozgan edik — u butun v1 router'ni ildizga ulagan. Natijada /eksportlar (Deprecation sarlavhasiz) va bugungi /auth/kirish ham versiyasiz manzilda ochiq edi. Hech kim bilmagan yo'l — hech kim himoya qilmaydigan yo'l. OWASP API Security Top 10 da bu API9 (Improper Inventory Management). Endi ildizda faqat /vazifalar va /teglar.
Uchala zaiflik bitta foydalanuvchili tizimda ko'rinmas edi — ikkinchi foydalanuvchi paydo bo'lishi bilan ochildi. Saboq: ko'p foydalanuvchili tizim testlari doim kamida ikki foydalanuvchi bilan yoziladi.
Yana bitta topilma — testlarning o'zida. auth.test.ts va yangi sessiya.test.ts bitta test bazasida parallel ishlab, bir-birining jadvallarini TRUNCATE qildi va 6 ta test yiqildi. node --test fayllarni alohida jarayonlarda parallel yurgizadi — 27-qismdagi xuddi shu muammo qaytdi. Yechim: har test fayli o'z bazasini shablondan oladi (CREATE DATABASE … TEMPLATE, ~0.3 s). Oldingi darsdagi beqaror timing testi ham shu holatda navbatma-navbat o'lchashga o'tkazildi (chegara 20 ms).
Tekshirib ko'ring: Nega begona vazifa so'ralganda 403 emas, 404 qaytariladi? 403 "ruxsat yo'q" degani-ku, aniqroq emasmi?
Javob
403 javobi "bu id'li vazifa bor, faqat sizniki emas" degan ma'lumotni beradi. Hujumchi id'larni terib, qaysilari mavjudligini (va taxminan nechta vazifa borligini) bilib oladi. 404 esa "sizning dunyoingizda bunday narsa yo'q" — begona obyekt va mavjud bo'lmagan obyekt bir xil ko'rinadi. Bu oldingi darsdagi user enumeration'ga qarshi bir xil javob g'oyasining davomi.
10. Hujumchi nigohi
Har qatorda: hujum, nega ishlaydi, himoya va uni qaysi test tekshiradi (sessiya.test.ts).
| Hujum | Nima uchun ishlaydi | Kanon himoyasi | Test |
|---|---|---|---|
| bazadan sessiya ID'larini o'g'irlash | store kaliti — ochiq ID | bazada faqat SHA-256 | bazada token yo'q |
| session fixation | login'da token o'zgarmaydi | har doim yangi token, eskisi bekor | eski cookie — 401 |
| o'g'irlangan tokenni oylab ishlatish | faqat idle muddat | mutlaq muddat 30 kun | muddat o'tgan — 401 |
| bot bilan sessiya jadvalini to'ldirish | saveUninitialized: true |
sessiya faqat login'da | — (tajriba: 0 qator) |
| soxta cookie bilan bazani band qilish | har qiymat so'rovga aylanadi | regex — bazaga bormaydi | ' OR 1=1 -- — 401 |
| boshqa foydalanuvchining Idempotency-Key'i | kalit umumiy | "<egasi>:<kalit>" |
Dilnoza — yangi 201 |
| begona eksport id'si | egasi tekshirilmaydi | egasi + 404 |
begona — 404 |
| unutilgan versiyasiz yo'l | butun router ildizga ulangan | faqat kerakli yo'llar | /eksportlar — 404 |
11. Ko'p uchraydigan xatolar
| Xato | Nega xavfli | To'g'risi |
|---|---|---|
| tokenni bazada ochiq saqlash | baza sizsa — hamma sessiya | sha256(token) |
Math.random() bilan token |
bashorat qilinadi | randomBytes(32) (CSPRNG) |
login'da regenerate yo'q |
session fixation | har doim yangi token |
| chiqishda faqat cookie o'chirish | token bazada tirik qoladi | bazada bekor_vaqti |
| faqat bitta muddat | o'g'irlangan token uzoq yashaydi | idle + absolute |
express-session sukutlari |
bo'sh sessiyalar, ogohlantirishlar | resave, saveUninitialized: false |
| begona obyektga 403 | borligini oshkor qiladi | 404 |
| bitta foydalanuvchi bilan test | BOLA ko'rinmaydi | kamida ikki foydalanuvchi |
12. Mashqlar
Mashqlar kurs/mashqlar/28/07-sessiya/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
- Sessiya tokeni tasodifiy bo'lgani uchun uni bazada argon2 bilan hash qilish shart.
saveUninitialized: truelogin qilmagan har tashrifchiga sessiya yozadi.- Chiqishda cookie'ni o'chirish kifoya — token boshqa ishlamaydi.
- Mutlaq muddat faol foydalanuvchini ham ma'lum vaqtdan keyin qayta login qildiradi.
- Begona vazifa so'ralganda kanon 404 qaytaradi.
Yechim
- Noto'g'ri — 256 bitli tasodifiy token uchun tez SHA-256 yetarli; tuzsiz hash qidiruvni ham imkonli qiladi.
- To'g'ri — kanon o'lchovi: 100 anonim so'rov — 100 qator.
- Noto'g'ri — token nusxasi hujumchida bo'lishi mumkin; sessiya bazada bekor qilinishi kerak.
- To'g'ri — kanonda 30 kun.
- To'g'ri — borligini ham oshkor qilmaslik uchun.
2-mashq (o'rta): sessiya holati
holat(s, hozir) funksiyasini yozing: s — { yaratilgan, oxirgi, bekor } (millisekundlar, bekor — null yoki vaqt). Qaytaradi: bekor (bekor qilingan), muddat (yaratilganidan 30 kun o'tgan), idle (oxirgi so'rovdan 7 kundan ko'p o'tgan) yoki faol. Yana yozilsinmi(s, hozir) — oxirgi faollik 60 soniyadan eski bo'lsa true. Beshta holatda sinang. Ishora: sana — Date.UTC (Date asoslari).
Yechim
const DAQIQA = 60_000;
const KUN = 24 * 60 * DAQIQA;
const SOZ = { idleMs: 7 * KUN, muddatMs: 30 * KUN };
function holat(s, hozir, soz = SOZ) {
if (s.bekor !== null) return "bekor";
if (hozir >= s.yaratilgan + soz.muddatMs) return "muddat";
if (hozir - s.oxirgi > soz.idleMs) return "idle";
return "faol";
}
const yozilsinmi = (s, hozir) => hozir - s.oxirgi > DAQIQA;
const t0 = Date.UTC(2026, 9, 1);
const s = { yaratilgan: t0, oxirgi: t0, bekor: null };
const SINOV = [
["30 soniya", 30_000],
["2 daqiqa", 2 * DAQIQA],
["8 kun jim", 8 * KUN],
];
for (const [nom, dt] of SINOV) {
console.log(nom, holat(s, t0 + dt), yozilsinmi(s, t0 + dt));
}
// har kuni faol, lekin 31 kun o'tdi
const faol = {
yaratilgan: t0, oxirgi: t0 + 31 * KUN - 1000, bekor: null,
};
console.log("31 kun faol", holat(faol, t0 + 31 * KUN));
console.log("chiqqan", holat({ ...s, bekor: t0 }, t0 + 1000));Konsolda:
30 soniya faol false
2 daqiqa faol true
8 kun jim idle true
31 kun faol muddat
chiqqan bekorTekshiruv tartibi muhim: avval bekor, keyin muddat, oxirida idle. "31 kun faol" sessiyada oxirgi so'rov bir soniya oldin bo'lgan — idle bo'yicha u tirik, lekin mutlaq muddat uni to'xtatdi. Kanon SQL so'rovida xuddi shu uch shart AND bilan turadi.
3-mashq (qiyin): xotiradagi sessiya ombori
XotiraSessiyalari klassini yozing. Tokenlar Map da faqat SHA-256 hash (hex) bo'yicha saqlansin: yarat(foydalanuvchi) — 32 baytli base64url token qaytaradi; tekshir(token) — foydalanuvchi yoki null; kirish(foydalanuvchi, eskiToken) — eski token bo'lsa uni bekor qiladi va har doim yangisini beradi; hammasiniBekorQil(foydalanuvchi) — nechta bekor qilinganini qaytaradi. Session fixation ssenariysini sinang: hujumchi o'z tokenini qurbonga "beradi", qurbon shu token bilan kiradi. Ishora: maxfiy maydon — #qatorlar (Private # maydon va metodlar).
Yechim
const enc = new TextEncoder();
const hex = (buf) => Array.from(new Uint8Array(buf),
(x) => x.toString(16).padStart(2, "0")).join("");
const b64url = (b) => btoa(String.fromCharCode(...b))
.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
const hash = async (t) =>
hex(await crypto.subtle.digest("SHA-256", enc.encode(t)));
class XotiraSessiyalari {
#qatorlar = new Map(); // hash → { foydalanuvchi, bekor }
async yarat(foydalanuvchi) {
const token = b64url(crypto.getRandomValues(new Uint8Array(32)));
const qator = { foydalanuvchi, bekor: false };
this.#qatorlar.set(await hash(token), qator);
return token;
}
async tekshir(token) {
const q = this.#qatorlar.get(await hash(token));
return q && !q.bekor ? q.foydalanuvchi : null;
}
// fixation himoyasi: eski token bekor, har doim yangisi
async kirish(foydalanuvchi, eskiToken) {
if (eskiToken) {
const q = this.#qatorlar.get(await hash(eskiToken));
if (q) q.bekor = true;
}
return this.yarat(foydalanuvchi);
}
hammasiniBekorQil(foydalanuvchi) {
let soni = 0;
for (const q of this.#qatorlar.values()) {
if (q.foydalanuvchi === foydalanuvchi && !q.bekor) {
q.bekor = true;
soni++;
}
}
return soni;
}
}
const s = new XotiraSessiyalari();
const hujumchi = await s.yarat("hujumchi");
// qurbon hujumchi cookie'si bilan kiradi
const aziz = await s.kirish("aziz", hujumchi);
console.log(aziz === hujumchi, await s.tekshir(hujumchi));
console.log(await s.tekshir(aziz));
const telefon = await s.kirish("aziz");
console.log(s.hammasiniBekorQil("aziz"), await s.tekshir(telefon));Konsolda:
false null
aziz
2 nullBirinchi qator — fixation yopildi: qurbon yangi token oldi, hujumchining tokeni esa umuman ishlamaydi (null). Oxirgi qator — ikkita qurilma (kompyuter va telefon) bitta chaqiruv bilan yopildi. Map kaliti — hash, shuning uchun xotira "sizsa" ham tokenlar ko'rinmaydi.
13. Loyiha qadami: sessiyalar
Bu holat katta: yangi jadval, ikki yangi fayl, router va ilova o'zgarishi, uchta zaiflik tuzatildi. Darsdagi kod telefon uchun bo'lingan, izohlari qisqartirilgan, ma'nosi kanondagidek. Bo'lingan sessiya.ts va middleware.ts ni kanon holat nusxasiga qo'yib, tip, lint va sessiya testlaridan o'tkazdik.
13.1 Migratsiya
sql/112-sessiyalar.sql = prisma/migrations/20261027120000_sessiyalar/migration.sql (+ schema.prisma da Sessiya modeli):
-- Brauzer cookie'da faqat tasodifiy token (32 bayt, base64url)
-- saqlaydi; bazada token EMAS, uning SHA-256 hash'i. Token yuqori
-- entropiyali — tuz va sekin hash kerak emas.
CREATE TABLE sessiyalar (
id bigint GENERATED ALWAYS AS IDENTITY,
foydalanuvchi_id bigint NOT NULL,
token_hash bytea NOT NULL,
yaratilgan_vaqt timestamptz NOT NULL DEFAULT now(),
oxirgi_faollik timestamptz NOT NULL DEFAULT now(), -- idle
tugash_vaqti timestamptz NOT NULL, -- absolute
bekor_vaqti timestamptz, -- chiqish
ip inet,
user_agent text,
CONSTRAINT sessiyalar_pkey PRIMARY KEY (id),
CONSTRAINT sessiyalar_foydalanuvchi_id_fkey
FOREIGN KEY (foydalanuvchi_id) REFERENCES foydalanuvchilar (id)
ON DELETE CASCADE,
CONSTRAINT sessiyalar_token_hash_key UNIQUE (token_hash),
CONSTRAINT sessiyalar_token_hash_check
CHECK (octet_length(token_hash) = 32),
CONSTRAINT sessiyalar_muddat_check
CHECK (tugash_vaqti > yaratilgan_vaqt),
CONSTRAINT sessiyalar_user_agent_check
CHECK (char_length(user_agent) <= 300)
);
-- "barcha qurilmalardan chiqish" — faqat faol sessiyalar
CREATE INDEX sessiyalar_foydalanuvchi_faol_idx
ON sessiyalar (foydalanuvchi_id) WHERE bekor_vaqti IS NULL;Qisman indeks (WHERE bekor_vaqti IS NULL) — bekor qilingan eski sessiyalar indeksga kirmaydi, u kichik qoladi.
13.2 src/auth/sessiya.ts
Oldingi darsdagi cookieOl() shu faylning oxirida; uni takrorlamadik.
// sessiya.ts — server sessiyalari (Postgres) va sessiya cookie'si.
// Token: 32 tasodifiy bayt (256 bit, CSPRNG) → cookie'da base64url;
// bazada faqat sha256(token). Tekshiruv — bitta indeksli so'rov.
import { createHash, randomBytes } from "node:crypto";
import type { Pool, QueryResultRow } from "pg";
import type { CookieOptions, Response } from "express";
import { TURLAR } from "../vazifalar/pg-ombor.ts";
import type { Foydalanuvchi } from "./ombor.ts";
export type SessiyaSozlamasi = {
/** bo'sh turish chegarasi (ms) */
idleMs: number;
/** mutlaq chegara (ms): faol bo'lsa ham shundan keyin qayta
* kirish */
muddatMs: number;
/** Secure atributi va `__Host-` prefiksi */
secure: boolean;
};
export const SUKUT: SessiyaSozlamasi = Object.freeze({
idleMs: 7 * 24 * 3600_000,
muddatMs: 30 * 24 * 3600_000,
secure: true,
});
/** `__Host-`: Secure + Path=/ + Domain'siz bo'lishi SHART — brauzer
* boshqacha cookie'ni rad etadi */
export const cookieNomi = (
s: Pick<SessiyaSozlamasi, "secure">,
): string => (s.secure ? "__Host-sessiya" : "sessiya");
/** oxirgi_faollik har so'rovda yozilmaydi (yozish yuklamasi) — 60 s
* dan eski bo'lsa yangilanadi */
const FAOLLIK_QADAMI_MS = 60_000;
export const tokenHash = (token: string): Buffer =>
createHash("sha256").update(token).digest();
export type JoriySessiya = {
id: number;
foydalanuvchi: Foydalanuvchi;
yaratilgan: Date;
tugash: Date;
};
type Qator = {
id: number;
foydalanuvchiId: number;
email: string;
toliqIsm: string;
yaratilgan: Date;
tugash: Date;
oxirgi: Date;
};
export class SessiyaOmbori {
readonly #pool: Pool;
readonly s: SessiyaSozlamasi;
constructor(pool: Pool, s: SessiyaSozlamasi = SUKUT) {
this.#pool = pool;
this.s = s;
}
async #sorov<T extends QueryResultRow>(
text: string,
values: unknown[],
): Promise<T[]> {
const r = await this.#pool.query<T>({
text,
values,
types: TURLAR,
});
return r.rows;
}
/** Yangi sessiya: token faqat qaytariladi (cookie'ga), bazaga
* hash */
async yarat(
foydalanuvchiId: number,
mijoz: {
ip?: string | undefined;
userAgent?: string | undefined;
},
): Promise<{ token: string; tugash: Date }> {
const token = randomBytes(32).toString("base64url");
const [q] = await this.#sorov<{ tugash: Date }>(
`INSERT INTO sessiyalar
(foydalanuvchi_id, token_hash, tugash_vaqti, ip, user_agent)
VALUES ($1, $2, now() + $3 * interval '1 millisecond', $4, $5)
RETURNING tugash_vaqti AS tugash`,
[
foydalanuvchiId,
tokenHash(token),
this.s.muddatMs,
mijoz.ip ?? null,
mijoz.userAgent?.slice(0, 300) ?? null,
],
);
if (q === undefined) throw new Error("sessiya yaratilmadi");
return { token, tugash: q.tugash };
}
/** Token → faol sessiya (bekor qilinmagan, mutlaq va idle muddati
* o'tmagan) yoki undefined */
async tekshir(token: string): Promise<JoriySessiya | undefined> {
// 32 bayt base64url — boshqa shakl bazaga ham bormaydi
if (!/^[A-Za-z0-9_-]{43}$/.test(token)) return undefined;
const [q] = await this.#sorov<Qator>(
`SELECT s.id, s.foydalanuvchi_id AS "foydalanuvchiId",
f.email, f.toliq_ism AS "toliqIsm",
s.yaratilgan_vaqt AS yaratilgan,
s.tugash_vaqti AS tugash, s.oxirgi_faollik AS oxirgi
FROM sessiyalar s
JOIN foydalanuvchilar f ON f.id = s.foydalanuvchi_id
WHERE s.token_hash = $1 AND s.bekor_vaqti IS NULL
AND s.tugash_vaqti > now()
AND s.oxirgi_faollik >
now() - $2 * interval '1 millisecond'`,
[tokenHash(token), this.s.idleMs],
);
if (q === undefined) return undefined;
if (Date.now() - q.oxirgi.getTime() > FAOLLIK_QADAMI_MS) {
await this.#sorov(
"UPDATE sessiyalar SET oxirgi_faollik = now() WHERE id = $1",
[q.id],
);
}
return {
id: q.id,
foydalanuvchi: {
id: q.foydalanuvchiId,
email: q.email,
toliqIsm: q.toliqIsm,
},
yaratilgan: q.yaratilgan,
tugash: q.tugash,
};
}
async bekorQil(sessiyaId: number): Promise<void> {
await this.#sorov(
"UPDATE sessiyalar SET bekor_vaqti = now() " +
"WHERE id = $1 AND bekor_vaqti IS NULL",
[sessiyaId],
);
}
async tokenniBekorQil(token: string): Promise<void> {
await this.#sorov(
"UPDATE sessiyalar SET bekor_vaqti = now() " +
"WHERE token_hash = $1 AND bekor_vaqti IS NULL",
[tokenHash(token)],
);
}
/** Barcha qurilmalardan chiqish; `qoldir` — joriy sessiya
* (masalan, parol o'zgartirgan qurilma) */
async hammasiniBekorQil(
foydalanuvchiId: number,
qoldir?: number,
): Promise<number> {
const r = await this.#pool.query(
"UPDATE sessiyalar SET bekor_vaqti = now() " +
"WHERE foydalanuvchi_id = $1 AND bekor_vaqti IS NULL " +
"AND id <> $2",
[foydalanuvchiId, qoldir ?? 0],
);
return r.rowCount ?? 0;
}
cookieQoy(res: Response, token: string, tugash: Date): void {
res.cookie(cookieNomi(this.s), token, {
...this.#atributlar(),
expires: tugash,
});
}
cookieOchir(res: Response): void {
res.clearCookie(cookieNomi(this.s), this.#atributlar());
}
#atributlar(): CookieOptions {
// HttpOnly — JS (XSS) o'qiy olmaydi; SameSite=Lax — boshqa
// saytdan POST/fetch/iframe'da yuborilmaydi, havola bilan — ha
return {
httpOnly: true,
secure: this.s.secure,
sameSite: "lax",
path: "/",
};
}
}Diqqat qiling: $3 * interval '1 millisecond' — muddat millisekundda parametr bilan uzatiladi. U SQL matniga yopishtirilmaydi (Parametrli so'rovlar va SQL injection). cookieOchir cookie'ni xuddi shu atributlar bilan o'chiradi — atributlar farq qilsa, brauzer uni boshqa cookie deb biladi va eskisi qoladi.
13.3 src/auth/middleware.ts
// middleware.ts — kim so'rayapti. kimlik() — cookie'dagi sessiyani
// tekshiradi va req.sessiya ni qo'yadi (bo'lmasa — jim o'tadi);
// kirishKerak — himoyalangan yo'llar uchun: sessiya yo'q → 401,
// bor → req.egasi = foydalanuvchi id. Rol va egalik — keyinroq.
import type { RequestHandler } from "express";
import { IlovaXatosi } from "../umumiy/xatolar.ts";
import { cookieNomi, cookieOl } from "./sessiya.ts";
import type { SessiyaOmbori } from "./sessiya.ts";
export class KirilmaganXatosi extends IlovaXatosi {
constructor() {
// RFC 9110: 401 javobida WWW-Authenticate bo'lishi kerak. Cookie
// sessiyasi uchun standart sxema yo'q — o'zimizniki, brauzer
// oynasini ochmaydi
super(401, "Avval tizimga kiring", {
kod: "kirish-kerak",
sarlavha: "Kirish kerak",
httpSarlavhalar: {
"WWW-Authenticate": 'Cookie realm="vazifalar"',
},
});
}
}
export function kimlik(sessiyalar: SessiyaOmbori): RequestHandler {
const nom = cookieNomi(sessiyalar.s);
return async (req, _res, next) => {
const token = cookieOl(req.headers.cookie, nom);
if (token !== undefined) {
const s = await sessiyalar.tekshir(token);
if (s !== undefined) req.sessiya = s;
}
next();
};
}
export const kirishKerak: RequestHandler = (req, _res, next) => {
if (req.sessiya === undefined) throw new KirilmaganXatosi();
req.egasi = req.sessiya.foydalanuvchi.id;
next();
};
/** DEV rejim (auth'siz, xotira/fayl ombori): hamma so'rov bitta
* foydalanuvchi nomidan — 25–27 qismlardagi xulq */
export function ozgarmasEgasi(egasiId: number): RequestHandler {
return (req, _res, next) => {
req.egasi = egasiId;
next();
};
}Ikki qatlam ataylab ajratilgan: kimlik — "kim?" (hech kimni to'xtatmaydi), kirishKerak — "kirgan bo'lishi shart" (to'xtatadi). Ommaviy yo'llar (/auth/kirish) faqat birinchisini ko'radi. src/express.d.ts da Request turiga sessiya? va egasi maydonlari qo'shiladi.
13.4 Router: sessiya berish va chiqish
src/auth/marshrut.ts ga qo'shilgan asosiy qism (/royxat va /kirish endi sessiyaBer ni chaqiradi; GET /men — joriy foydalanuvchi va sessiya muddati):
/** Session fixation himoyasi: so'rov bilan kelgan eski cookie
* (hujumchi oldindan qo'ygan bo'lishi mumkin) bekor qilinadi va
* HAR DOIM yangi token beriladi (express-session'dagi
* `req.session.regenerate()` vazifasi) */
async function sessiyaBer(
req: Request, res: Response, f: Foydalanuvchi,
): Promise<void> {
const eski = cookieOl(req.headers.cookie, cookieNomi(sessiyalar.s));
if (eski !== undefined) await sessiyalar.tokenniBekorQil(eski);
const { token, tugash } = await sessiyalar.yarat(f.id, mijoz(req));
sessiyalar.cookieQoy(res, token, tugash);
}
// Chiqish: sessiya bo'lmasa ham 204 (idempotent)
r.route("/chiqish")
.post(async (req, res) => {
if (req.sessiya !== undefined) {
await sessiyalar.bekorQil(req.sessiya.id);
await xizmat.hodisa({
tur: "chiqish",
foydalanuvchiId: req.sessiya.foydalanuvchi.id,
...mijoz(req),
});
}
sessiyalar.cookieOchir(res);
res.sendStatus(204);
})
.all(metodYoq("POST"));/chiqish-hammasi xuddi shunday: sessiya yo'q bo'lsa 401, bor bo'lsa hammasiniBekorQil va { bekorQilindi: soni }. Jurnalga chiqish va chiqish_hammasi hodisalari yoziladi (HodisaTuri ga ikki yangi tur, xizmat.ts ga hodisa metodi).
13.5 Ilova: har so'rov o'z ombori
src/ilova.ts dagi asosiy o'zgarish (diff qisqartirilgan va bo'lingan):
- ombor: VazifaOmbori;
+ /** bitta ombor (dev) yoki foydalanuvchi bo'yicha fabrika */
+ ombor: VazifaOmbori | OmborFabrikasi;
- auth?: AuthOmbori | undefined;
+ auth?:
+ | { ombor: AuthOmbori; sessiyalar: SessiyaOmbori }
+ | undefined;
…
+ const egasiniAniqla =
+ auth === undefined ? ozgarmasEgasi(egasiId) : kirishKerak;
+ if (auth !== undefined) app.use(kimlik(auth.sessiyalar));
+ const omborOl = (req: Request): VazifaOmbori =>
+ typeof ombor === "function" ? ombor(req.egasi) : ombor;
+ const malumot = express.Router();
+ malumot.use("/vazifalar", egasiniAniqla,
+ idempotentlik(idempotentlikOmbori),
+ vazifalarRouteri(xizmatOl, { tz }));
+ malumot.use("/teglar", egasiniAniqla, teglarRouteri(xizmatOl));
…
- app.use(eskirgan(ESKI_YOLLAR), v1);
+ // faqat /vazifalar va /teglar — butun v1 emas (yashirin yo'llar)
+ app.use(eskirgan(ESKI_YOLLAR), malumot);Postgres rejimida server.ts fabrika beradi: (egasi) => new PgOmbori(pool, egasi) — har so'rovga yangi, arzon obyekt, ulanishlar esa umumiy hovuzda. EGASI_ID endi faqat dev rejim (xotira yoki fayl) uchun.
BOLA tuzatishlari — ikki kichik diff:
// src/umumiy/idempotentlik.ts
+ // kalit global edi — Dilnoza Azizning kaliti bilan uning
+ // saqlangan javobini olardi (BOLA). Endi foydalanuvchi bo'yicha
+ const ichkiKalit = `${String(req.egasi)}:${kalit}`;
- const holat = await ombor.boshla(kalit, izi, muddatMs);
+ const holat = await ombor.boshla(ichkiKalit, izi, muddatMs);
// src/eksport/eksport.ts
-type Ish = { id: string; sorov: EksportSorovi; …
+type Ish = { id: string; egasi: number; sorov: EksportSorovi; …
- if (ish === undefined) throw new IlovaXatosi(404, …
+ // begona ish — "topilmadi" (403 emas)
+ if (ish?.egasi !== egasi) throw new IlovaXatosi(404, …src/config.ts ga uch sozlama: SESSIYA_IDLE_DAQIQA (10 080 — 7 kun), SESSIYA_MUDDAT_KUN (30), COOKIE_SECURE (true). Va ikki fail-fast qoida: production'da SAQLASH=xotira|fayl (auth'siz rejim) va COOKIE_SECURE=false — rad.
13.6 Testlar
test/sessiya.test.ts — 16 ta test. Ular: cookieOl, Set-Cookie atributlari (regex bilan, ~30 kun, tanada token yo'q), bazada faqat hash, 401 va WWW-Authenticate, soxta cookie'lar (' OR 1=1 -- ham), COOKIE_SECURE=false, IDOR (begona id — 404). Yana: Idempotency-Key, eksport, yashirin yo'llar, chiqish, chiqish-hammasi, fixation, idle va mutlaq muddat, 60 soniya qoidasi, jurnal. Idempotency testi (bo'lingan):
test("Idempotency-Key — foydalanuvchi bo'yicha: Dilnoza Azizning " +
"kalitini ishlatsa Azizning javobini OLMAYDI", async () => {
const aziz = await royxat("aziz@example.com");
const dilnoza = await royxat("dilnoza@example.com");
const a = await request(app).post("/v1/vazifalar")
.set("Cookie", aziz).set("Idempotency-Key", "kalit-123")
.send({ matn: "Bir xil matn" }).expect(201);
const d = await request(app).post("/v1/vazifalar")
.set("Cookie", dilnoza).set("Idempotency-Key", "kalit-123")
.send({ matn: "Bir xil matn" });
assert.equal(d.headers["idempotency-replayed"], undefined,
"Azizning saqlangan javobi Dilnozaga qaytdi");
assert.equal(d.status, 201);
assert.notEqual((d.body as { id: number }).id,
(a.body as { id: number }).id);
});Kanon testining oxirida Azizning o'z takrori ham tekshiriladi (Idempotency-Replayed: true). Biz testlarni kanon holat nusxasida, yuqoridagi bo'lingan fayllar bilan ishga tushirdik (Node 24.21.0, PostgreSQL 17.6):
node --test test/sessiya.test.ts✔ cookieOl — nomi aniq mos kelgan birinchi cookie (167.9968ms)
✔ cookie va kirish (1986.8178ms)
▶ foydalanuvchilar ajratilgan (IDOR / BOLA)
✔ Aziz va Dilnoza: har biri faqat o'z vazifalari; begona id — 404 (403 emas); PATCH/DELETE begonaga — 404 (412.749ms)
✔ Idempotency-Key — foydalanuvchi bo'yicha: Dilnoza Azizning kalitini ishlatsa Azizning javobini OLMAYDI (325.9077ms)
✔ eksport ishi — faqat yaratgan foydalanuvchiga (begona — 404) (505.8299ms)
✔ yashirin yo'llar yo'q: /eksportlar, /auth versiyasiz — 404 (faqat /vazifalar, /teglar eski yo'l sifatida qoldi) (257.2168ms)
✔ foydalanuvchilar ajratilgan (IDOR / BOLA) (1502.5133ms)
✔ chiqish, fixation, muddatlar (1774.8589ms)
ℹ tests 16
ℹ pass 16
ℹ fail 0Chiqishni qisqartirdik: ikki to'plamning ichki qatorlari va ba'zi ℹ qatorlari olib tashlandi. Oldingi darsdagi auth.test.ts ham (navbatma-navbat o'lchaydigan yangi timing testi bilan) shu nusxada 10/10 o'tdi — band mashinada ham.
13.7 Jonli API va commit
Kanon tekshiruvi (2026-10-27, PostgreSQL 18.6): tip va lint xatosiz, testlar 144 (143 + 1 o'tkazilgan), sessiya testlari alohida 16/16, migratsiya va schema.prisma farqsiz. Jonli so'rovlardan (token va sanani qisqartirdik):
$ curl http://127.0.0.1:4307/v1/vazifalar?limit=1
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Cookie realm="vazifalar"
$ curl -X POST … /v1/auth/kirish (Aziz, namuna paroli) → Set-Cookie
HTTP/1.1 200 OK
Set-Cookie: __Host-sessiya=<token>; Path=/; Expires=Thu, 26 Nov 2026 14:35:34 GMT; HttpOnly; Secure; SameSite=Lax
$ curl -H Cookie: __Host-sessiya=<token> http://127.0.0.1:4307/v1/vazifalar/14
HTTP/1.1 404 Not Found
$ curl -H Cookie: __Host-sessiya=<token> -X POST http://127.0.0.1:4307/v1/auth/chiqish
HTTP/1.1 204 No Content
Set-Cookie: __Host-sessiya=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=LaxVazifa 14 — Dilnozaniki: Aziz uni ko'rmaydi (404). Chiqishdan keyin o'sha cookie bilan so'rov yana 401 qaytardi.
Oldingi darsdagi cookieOl bilan birga hammasini bitta commit qilamiz:
M="feat(auth): server sessiyalari (Postgres, token hash), __Host-"
M+=" cookie, chiqish va barcha qurilmalardan chiqish; har so'rov"
M+=" o'z foydalanuvchisi; fix: idempotentlik kaliti va eksport"
M+=" foydalanuvchi bo'yicha, yashirin versiyasiz yo'llar yopildi;"
M+=" db: 112 sessiyalar"
git add src test sql prisma
git commit -m "$M"14. Real ishda
- Freymvorklar sessiyani tayyor beradi, lekin sukut sozlamalarini tekshirish — sizning ishingiz:
express-sessionmisolidagi kabi ogohlantirishlar ko'pincha e'tiborsiz qoladi. - OWASP Session Management Cheat Sheet tavsiyalari kanon bilan mos: kamida 64 bit entropiya (bizda 256), login'da ID yangilanishi, idle va absolute timeout, server tomonida bekor qilish.
- Sessiya va GDPR/shaxsiy ma'lumot:
ipvauser_agent— shaxsiy ma'lumot hisoblanishi mumkin; ular qurilmalar ro'yxati uchun kerak, lekin eski bekor qilingan qatorlarni vaqti-vaqti bilan tozalash rejasi bo'lsin. - Intervyuda: "Sessiya va JWT farqi?", "Session fixation nima?", "Nega sessiya tokenini hash qilasiz, lekin tuzsiz?", "Idle va absolute timeout?".
Xulosa
- Sessiya: brauzerda tasodifiy token (
__Host-sessiya,HttpOnly,Secure,SameSite=Lax), serverda — kim va qachongacha. express-sessionsukutlari: 100 anonim so'rov — 100 qator;resave: false,saveUninitialized: false; store kaliti — ochiq ID.- Kanon: 32 bayt CSPRNG token, bazada SHA-256 (yuqori entropiya — tuz va argon2 shart emas),
CHECK32 bayt. - Idle (7 kun) va absolute (30 kun) muddat;
oxirgi_faollikfaqat 60 soniyada bir yoziladi. - Fixation — login'da har doim yangi token; chiqish — bazada darhol; "barcha qurilmalardan chiqish" — bitta
UPDATE. - Ikkinchi foydalanuvchi bilan uchta BOLA/API9 zaifligi ochildi va yopildi; begona obyektga — 404.
Keyingi dars: JWT anatomiyasi — sessiyaning muqobili: imzolangan, lekin shifrlanmagan token, alg: none hujumi va kalit aylantirish.
Manbalar
- OWASP Session Management Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- express-session README (resave, saveUninitialized, regenerate): https://github.com/expressjs/session
- OWASP API Security Top 10 2023 — API1 (BOLA), API9 (Improper Inventory Management): https://owasp.org/API-Security/editions/2023/en/0x11-t10/
- RFC 9110 — 401 va
WWW-Authenticate: https://www.rfc-editor.org/rfc/rfc9110#name-401-unauthorized - Kanon o'lchovi (IlmHamroh, 2026-10-27): sessiya store'lari tajribasi (express-session 1.19.0, connect-pg-simple 10.0.0, connect-redis 10.0.0, Redis 8.10.2, PostgreSQL 18.6), holat 07 tekshiruvi. Brauzer misollari, SQL so'rovlar (PostgreSQL 17.6),
sessiya.test.tsvaauth.test.ts— biz ishga tushirdik (2026-10-28, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!