IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi7/31-dars29 daqiqa
Mundarija (34)

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-session o'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 bitta UPDATE, darhol. Foydalanuvchi paydo bo'lgach, 25-qismdan beri yashirinib yotgan uchta zaiflik ochildi: begona Idempotency-Key bilan 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-session sukut 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-api ga 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'chiriladi

Brauzer 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.

text
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

js
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:

text
store kaliti (sid) cookie ichidagi bilan bir xil: true

Ya'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:

js
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:

text
43 true
32 true
ee718602fc388f86

Token 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:

sql
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:

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

INSERT 0 1

Noto'g'ri uzunlikdagi "hash" (16 baytli MD5) — rad:

sql
\set VERBOSITY terse
INSERT INTO sessiyalar (foydalanuvchi_id, token_hash, tugash_vaqti)
VALUES (1, decode(md5('token-B'), 'hex'), now() + interval '1 day');

Natija:

text
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:

sql
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:

text
 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:

sql
UPDATE sessiyalar SET oxirgi_faollik = now() - interval '8 days'
WHERE id = 1;

Natija:

text
UPDATE 1
sql
SELECT 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:

text
 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):

ts
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:

Sessiyani tekshirish narxi (mediana, 1000 marta)
  • 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 bitta UPDATE bilan. Telefon yo'qolsa yoki parol sizsa — kerak bo'ladigan tugma.

Bazada sinaymiz — Azizga uchta qurilmadan yangi sessiya ochamiz va hammasini yopamiz:

sql
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:

text
INSERT 0 3
sql
UPDATE sessiyalar SET bekor_vaqti = now()
WHERE foydalanuvchi_id = 1 AND bekor_vaqti IS NULL;

Natija:

text
UPDATE 4

To'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

  1. Sessiya tokeni tasodifiy bo'lgani uchun uni bazada argon2 bilan hash qilish shart.
  2. saveUninitialized: true login qilmagan har tashrifchiga sessiya yozadi.
  3. Chiqishda cookie'ni o'chirish kifoya — token boshqa ishlamaydi.
  4. Mutlaq muddat faol foydalanuvchini ham ma'lum vaqtdan keyin qayta login qildiradi.
  5. Begona vazifa so'ralganda kanon 404 qaytaradi.
Yechim
  1. Noto'g'ri — 256 bitli tasodifiy token uchun tez SHA-256 yetarli; tuzsiz hash qidiruvni ham imkonli qiladi.
  2. To'g'ri — kanon o'lchovi: 100 anonim so'rov — 100 qator.
  3. Noto'g'ri — token nusxasi hujumchida bo'lishi mumkin; sessiya bazada bekor qilinishi kerak.
  4. To'g'ri — kanonda 30 kun.
  5. 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
js
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:

text
30 soniya faol false
2 daqiqa faol true
8 kun jim idle true
31 kun faol muddat
chiqqan bekor

Tekshiruv 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
js
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:

text
false null
aziz
2 null

Birinchi 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):

sql
-- 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.

ts
// 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

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):

ts
/** 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):

diff
-  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:

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):

ts
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):

bash
node --test test/sessiya.test.ts
text
✔ 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 0

Chiqishni 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):

text
$ 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=Lax

Vazifa 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:

bash
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-session misolidagi 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: ip va user_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-session sukutlari: 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), CHECK 32 bayt.
  • Idle (7 kun) va absolute (30 kun) muddat; oxirgi_faollik faqat 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Session-based auth: server sessiyalari, fixation va "barcha qurilmalardan chiqish" — IlmHamroh