Mundarija (35)
- Bu darsda
- 1. Nega bu kerak?
- 1.1 CORS — devordagi eshik, devor emas
- 1.2 Cookie qo'shilganda nima o'zgaradi
- 2. Origin va sayt
- 2.1 Origin — uchlik
- 2.2 Kanon topilmasi: oxiridagi /
- 2.3 Sayt — kengroq tushuncha
- 3. Oddiy so'rov va preflight
- 3.1 Qaysi so'rov oldidan ruxsat so'raladi
- 3.2 Server preflight'ga nima javob beradi
- 3.3 Max-Age ning chegarasi bor
- 4. credentials va *
- 4.1 Uch tomonning roziligi
- 4.2 "Aks ettirish" — eng xavfli yechim
- 4.3 Cookie baribir ketmasligi mumkin: SameSite
- 5. Origin tekshiruvining tuzoqlari
- 6. CORS bajarilishni to'xtatmaydi
- 6.1 Server javob beradi, brauzer yashiradi
- 6.2 Yozuvchi so'rovni kim to'xtatadi
- 7. Hujumchi nigohi: hujum → himoya → test
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): xavfsiz origin tekshiruvi
- 3-mashq (qiyin): CORS javobini yig'ish
- 10. Loyiha qadami: CORS credentials va brauzer testlari
- 10.1 src/ilova.ts
- 10.2 Birlik testi
- 10.3 Brauzer testlari: e2e/cors.e2e.ts
- 10.4 Halol eslatma: tasodifiy yiqiladigan test
- 10.5 Tekshiruv va commit
- 11. Real ishda
- Xulosa
- Manbalar
CORS chuqur: credentials, preflight, * tuzog'i va ruxsat ro'yxati
Qisqacha: CORS (Cross-Origin Resource Sharing) — himoya emas, ruxsat: brauzer boshqa origin'dagi javobni JS'ga faqat server "mumkin" desa beradi. So'rovning o'zi baribir serverga yetib boradi va bajariladi. Cookie bilan ishlaydigan API uchun uchta qoida: aniq origin (
*emas),Access-Control-Allow-Credentials: trueva frontend'dacredentials: "include". Bu darsdavazifalar-apiCORS'ini cookie sessiyasiga moslaymiz va uni haqiqiy brauzerda to'rtta test bilan tekshiramiz.
Bu darsda
- Origin va sayt farqini, same-origin policy CORS bilan qanday bog'liqligini takrorlaysiz.
- Qaysi so'rov preflight talab qilishini va server unga nima javob berishini o'qiysiz.
credentials, cookie va*qoidasini brauzer nuqtai nazaridan tushuntirasiz.- Origin tekshiruvidagi to'rtta keng tarqalgan xatoni topasiz.
vazifalar-apigacredentials: trueni va CORS'ning brauzer testlarini qo'shib, holatni commit qilasiz.
Oldin bilishingiz kerak: CORS mijoz tomondan, Mashhur middleware'lar (cors paketi va preflight), Brauzer xavfsizlik modeli (same-origin, SameSite), Helmet va xavfsizlik sarlavhalari, Tokenni qayerda saqlash va CSRF.
11-qismda CORS'ni brauzer tomondan ko'rdingiz: "CORS xatosi" konsolda qanday ko'rinadi. 25-qismda cors middleware'ini ruxsat ro'yxati bilan qo'ydik. O'shanda API kimligini so'ramasdi — cookie yo'q edi. Endi vazifalar-api da sessiya cookie'si bor, va CORS'ning eng chalkash qismi aynan shu yerda boshlanadi.
1. Nega bu kerak?
1.1 CORS — devordagi eshik, devor emas
Same-origin policy — brauzerdagi devor: http://localhost:5173 dagi sahifa http://localhost:3000 dagi javobni o'qiy olmaydi. CORS — shu devordagi eshik: server javobga Access-Control-Allow-Origin qo'ysa, brauzer eshikni ochadi.
Kanon 28-qism boshida 27-qism yakunidagi API'ni begona origin bilan sinagan edi (kanon tajribasi, curl):
$ curl -H Origin: https://yovuz.example http://127.0.0.1:4390/v1/vazifalar?limit=1&fields=id
HTTP/1.1 200 OK
...
{"versiya":14,"vazifalar":[{"id":1}],"sahifa":{"limit":1,"keyingi":"eyJ2IjpbMSwxXSwiaSI6IltbXSx7fV0ifQ"}}
$ curl -X DELETE -H Origin: https://yovuz.example http://127.0.0.1:4390/v1/vazifalar/2
HTTP/1.1 204 No ContentJavobda Access-Control-Allow-Origin yo'q edi — lekin server ma'lumotni berdi va o'chirdi. curl uchun CORS mavjud emas. Brauzer uchun esa farq bor: u javobni JS'ga bermaydi, lekin DELETE baribir bajarilgan bo'lardi (preflight bo'lmasa — pastda ko'ramiz).
Bu darsning asosiy fikri: CORS o'qishni cheklaydi, yozishni emas. Yozishdan himoya — CSRF tekshiruvi va autentifikatsiya.
1.2 Cookie qo'shilganda nima o'zgaradi
Sessiya cookie'si brauzerda saqlanadi va so'rovga o'zi qo'shiladi. Agar API har qanday origin'ga "javobni o'qi" desa, istalgan sayt foydalanuvchining brauzeri orqali uning vazifalarini o'qiydi. Shuning uchun cookie bilan CORS'da brauzer qo'shimcha qoidalar qo'yadi — ularni bugun o'rganamiz.
2. Origin va sayt
2.1 Origin — uchlik
Origin — sxema + xost + port. Yo'l, query va oxiridagi / origin'ga kirmaydi:
const MANZILLAR = [
"http://localhost:5173",
"http://localhost:5173/",
"https://login.github.io/vazifalar/",
"HTTPS://ILM.UZ:443",
"http://127.0.0.1:5173",
];
for (const m of MANZILLAR) {
console.log(m.padEnd(35), "→", new URL(m).origin);
}Konsolda:
http://localhost:5173 → http://localhost:5173
http://localhost:5173/ → http://localhost:5173
https://login.github.io/vazifalar/ → https://login.github.io
HTTPS://ILM.UZ:443 → https://ilm.uz
http://127.0.0.1:5173 → http://127.0.0.1:5173URL.origin kichik harfga o'tkazadi va sukutdagi portni (:443) olib tashlaydi. Brauzer Origin sarlavhasini aynan shu shaklda yuboradi. Oxirgi qatorga qarang: localhost va 127.0.0.1 — bir kompyuter, lekin ikki xil origin.
2.2 Kanon topilmasi: oxiridagi /
cors paketi ruxsat ro'yxatini aniq satr sifatida solishtiradi. 25-qism mashqida shunday teshik chiqqan edi: CORS_ORIGINS="https://login.github.io/" (oxirida /) — URL sifatida yaroqli, lekin brauzer Origin: https://login.github.io yuboradi. Satrlar teng emas — Access-Control-Allow-Origin chiqmaydi, frontend "CORS xatosi" ko'radi.
Kanon config.ts da har elementni URL.origin ga keltiradi va yo'l, ? yoki # bo'lsa ishga tushishni to'xtatadi. Kanon kodi (izohni qisqartirdik, qatorlarni bo'ldik):
// CORS origin: sxema + host (+ port). Oxirida "/" — URL yaroqli,
// lekin Origin sarlavhasida "/" yo'q: cors() aynan solishtiradi.
// Shuning uchun URL.origin ga normallanadi; yo'l, query, hash — rad.
const Origin = z.url({
error: "CORS_ORIGINS — vergul bilan ajratilgan URL'lar",
}).transform((x, ctx) => {
const u = new URL(x);
if (u.pathname !== "/" || u.search !== "" || u.hash !== "") {
ctx.issues.push({
code: "custom", input: x,
message: "CORS_ORIGINS elementi faqat origin bo'lsin",
});
return z.NEVER;
}
return u.origin;
});Xato sozlama — ishga tushishda darhol xato (Env validatsiyasi va tipli config obyekti). Ish paytida jim "CORS xatosi" emas.
2.3 Sayt — kengroq tushuncha
Sayt (site) — sxema + "ro'yxatdan o'tgan domen": https://app.ilm.uz va https://api.ilm.uz — ikki origin, lekin bir sayt (ilm.uz). localhost:5173 va localhost:4216 ham bir sayt. SameSite cookie atributi aynan saytga qaraydi, origin'ga emas (Brauzer xavfsizlik modeli). Bu farq birozdan keyin hal qiluvchi bo'ladi.
3. Oddiy so'rov va preflight
3.1 Qaysi so'rov oldidan ruxsat so'raladi
Brauzer ba'zi so'rovlarni darhol yuboradi va javobni keyin tekshiradi. Qolganlari oldidan OPTIONS — preflight yuboradi. MDN bo'yicha "oddiy" so'rov shartlari: metod GET, HEAD yoki POST; qo'lda qo'yilgan sarlavhalar faqat Accept, Accept-Language, Content-Language, Content-Type (va cheklangan Range); Content-Type faqat uchta turdan biri.
const ODDIY_METOD = ["GET", "HEAD", "POST"];
const ODDIY_SARLAVHA = [
"accept", "accept-language", "content-language", "content-type",
];
const ODDIY_TUR = [
"application/x-www-form-urlencoded", "multipart/form-data",
"text/plain",
];
function preflightmi(metod, sarlavhalar = {}) {
if (!ODDIY_METOD.includes(metod)) return `ha — metod ${metod}`;
for (const [nom, qiymat] of Object.entries(sarlavhalar)) {
const n = nom.toLowerCase();
if (!ODDIY_SARLAVHA.includes(n)) return `ha — sarlavha ${nom}`;
const tur = qiymat.split(";")[0].trim().toLowerCase();
if (n === "content-type" && !ODDIY_TUR.includes(tur)) {
return `ha — Content-Type ${tur}`;
}
}
return "yo'q — oddiy so'rov";
}
const JSON_TURI = { "Content-Type": "application/json" };
console.log(preflightmi("GET"));
console.log(preflightmi("POST", { "Content-Type": "text/plain" }));
console.log(preflightmi("POST", JSON_TURI));
console.log(preflightmi("PATCH", JSON_TURI));
console.log(preflightmi("GET", { Authorization: "Bearer vz_…" }));
console.log(preflightmi("DELETE"));Konsolda:
yo'q — oddiy so'rov
yo'q — oddiy so'rov
ha — Content-Type application/json
ha — metod PATCH
ha — sarlavha Authorization
ha — metod DELETEIkkinchi qator — xavfli joy: text/plain bilan POST preflight'siz ketadi va server uni bajaradi. Shuning uchun 25-qismda API faqat JSON turlarini qabul qiladi (415), text/plain dagi "JSON" o'tmaydi. Bizning frontend har doim JSON yuboradi — demak har yozuvchi so'rov oldidan preflight bo'ladi.
3.2 Server preflight'ga nima javob beradi
vazifalar-api ga ruxsat etilgan origin'dan preflight (biz ishga tushirdik: shu darsning kodi, Node 24.21.0; helmet sarlavhalarini olib tashladik):
curl -i -X OPTIONS "$A/v1/vazifalar/1" \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: PATCH" \
-H "Access-Control-Request-Headers: content-type,if-match"Javobdagi Access-Control-* qatorlariga qarang — brauzer qarorni faqat shularga qarab qiladi:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization,Idempotency-Key,X-Request-Id,If-Match,If-None-Match
Access-Control-Max-Age: 600
Access-Control-Expose-Headers: Location,X-Request-Id,Idempotency-Replayed,ETag,Link,Deprecation,Sunset
Content-Length: 0| Sarlavha | Ma'nosi |
|---|---|
Allow-Origin |
aynan so'ragan origin — ro'yxatda bor |
Vary: Origin |
javob origin'ga qarab farq qiladi — keshlar buni hisobga olsin |
Allow-Credentials: true |
cookie bilan so'rovga ruxsat |
Allow-Methods |
qaysi metodlar mumkin |
Allow-Headers |
qaysi sarlavhalarni yuborish mumkin — aniq ro'yxat |
Max-Age: 600 |
bu javobni 10 daqiqa eslab qol, qayta so'rama |
Endi xuddi shu preflight, lekin Origin: https://yovuz.example. Oldingi javob bilan qator-ma-qator solishtiring:
HTTP/1.1 204 No Content
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization,Idempotency-Key,X-Request-Id,If-Match,If-None-Match
Access-Control-Max-Age: 600
Access-Control-Expose-Headers: Location,X-Request-Id,Idempotency-Replayed,ETag,Link,Deprecation,Sunset
Content-Length: 0Status ham 204, metodlar ham bor — faqat Allow-Origin yo'q. Shu bitta qator yetarli: brauzer preflight'ni muvaffaqiyatsiz deb hisoblaydi va asosiy PATCH ni umuman yubormaydi. cors paketi qolgan sarlavhalarni hamma origin'ga qo'yadi — bu zararsiz, qaror Allow-Origin da.
3.3 Max-Age ning chegarasi bor
MDN bo'yicha brauzerlar Access-Control-Max-Age ni o'z chegarasigacha qisqartiradi: Chromium — 2 soat (7 200 s), Firefox — 24 soat. Sarlavha bo'lmasa — 5 soniya. Kanondagi 600 (10 daqiqa) ikkala chegaradan ham past — siyosatni o'zgartirsangiz, brauzerlar 10 daqiqadan keyin yangisini oladi.
4. credentials va *
4.1 Uch tomonning roziligi
Cookie bilan boshqa origin'ga so'rov uchun uchala shart kerak:
- Frontend:
fetch(url, { credentials: "include" }). Sukut"same-origin"— boshqa origin'ga cookie yuborilmaydi. - Server:
Access-Control-Allow-Credentials: true. - Server:
Access-Control-Allow-Origin— aniq origin,*emas.
MDN bo'yicha credentials bilan javobda * to'rtta sarlavhada ham ishlamaydi: Allow-Origin, Allow-Headers, Allow-Methods, Expose-Headers. Shuning uchun kanonda ularning hammasi aniq ro'yxat. Yana bir tafsilot: Allow-Origin: * bo'lsa, brauzer javobdagi Set-Cookie ni ham e'tiborsiz qoldiradi.
Brauzer qarorini soddalashtirilgan funksiya bilan ko'raylik:
// brauzer javobni JS'ga beradimi — soddalashtirilgan CORS tekshiruvi
function corsTekshir(origin, credentials, javob) {
const acao = javob["access-control-allow-origin"];
if (acao === undefined) return "TypeError: Allow-Origin yo'q";
if (credentials === "include") {
if (acao === "*") {
return "TypeError: credentials bilan * bo'lmaydi";
}
if (javob["access-control-allow-credentials"] !== "true") {
return "TypeError: Allow-Credentials yo'q";
}
}
if (acao !== "*" && acao !== origin) {
return "TypeError: origin boshqa";
}
return "javob JS'ga berildi";
}
const FRONT = "http://localhost:5173";
const API = {
"access-control-allow-origin": FRONT,
"access-control-allow-credentials": "true",
};
const BEGONAGA = { "access-control-allow-credentials": "true" };
const YULDUZ = {
"access-control-allow-origin": "*",
"access-control-allow-credentials": "true",
};
const BOSHQA = "http://localhost:8080";
console.log(corsTekshir(FRONT, "include", API));
console.log(corsTekshir(BOSHQA, "include", BEGONAGA));
console.log(corsTekshir(FRONT, "include", YULDUZ));
console.log(corsTekshir(FRONT, "omit", YULDUZ));Konsolda:
javob JS'ga berildi
TypeError: Allow-Origin yo'q
TypeError: credentials bilan * bo'lmaydi
javob JS'ga berildiTo'rtta qator — kanon brauzer testlaridagi to'rtta holatning soddalashtirilgan nusxasi. TypeError — brauzerda fetch aynan shu xato bilan rad etiladi; tafsilotni JS ko'rmaydi, faqat konsolda "CORS policy" xabari chiqadi. Haqiqiy natijalar — «Loyiha qadami» bo'limida.
4.2 "Aks ettirish" — eng xavfli yechim
* cookie bilan ishlamaganini ko'rgan dasturchi ko'pincha shunday qiladi: kelgan Origin ni o'qib, xuddi shuni Allow-Origin ga yozadi (cors({ origin: true, credentials: true })). Brauzer qoniqadi — origin aniq. Lekin bu har qanday saytga foydalanuvchi nomidan javobni o'qish huquqi: * dan ham yomon, chunki * hech bo'lmaganda cookie'ni yubormaydi.
4.3 Cookie baribir ketmasligi mumkin: SameSite
CORS ruxsat bersa ham, cookie'ni yuborish-yubormaslikni cookie'ning o'z atributlari hal qiladi. vazifalar-api cookie'si — SameSite=Lax. Lax cookie boshqa saytga fetch so'rovida yuborilmaydi.
Kanon testlari frontend va API'ni localhost ning har xil portlarida ishga tushiradi — ikki origin, lekin bir sayt. Shuning uchun cookie ketdi. Frontend GitHub Pages'da (login.github.io), API esa boshqa domenda bo'lsa — bu boshqa sayt: Lax cookie yuborilmaydi va CORS sozlamasi to'g'ri bo'lsa ham /men 401 qaytaradi. Kanon bu variantni sinamagan. Hujjat bo'yicha yechimlar: cookie'ni SameSite=None; Secure qilish (CSRF xavfi oshadi va ko'p brauzerlar uchinchi tomon cookie'larini baribir cheklaydi) yoki frontend va API'ni bitta saytga qo'yish (ilm.uz va api.ilm.uz, yoki proxy orqali bitta origin). Kanon ikkinchisini tanlagan: 22-qism frontend'i Vite proxy orqali API bilan bir origin'da ishlaydi va unga credentials ham kerak bo'lmadi.
Tekshirib ko'ring: Frontend
https://app.ilm.uz, APIhttps://api.ilm.uz, cookieSameSite=Lax, CORS ro'yxatidahttps://app.ilm.uzbor, frontendcredentials: "include"bilan so'raydi. Cookie ketadimi va javobni JS o'qiy oladimi?
Javob
Ha, ikkalasi ham. app.ilm.uz va api.ilm.uz — ikki origin (shuning uchun CORS kerak), lekin bir sayt (ilm.uz) — Lax cookie bir sayt ichidagi so'rovlarda yuboriladi. Server aniq origin va Allow-Credentials: true qaytaradi — brauzer javobni JS'ga beradi.
5. Origin tekshiruvining tuzoqlari
Ro'yxat katta bo'lsa yoki subdomenlar ko'p bo'lsa, dasturchi "qoida" yozmoqchi bo'ladi. Mana to'rtta keng tarqalgan variant, beshta origin ustida:
const RUXSAT = new Set(["https://ilm.uz", "https://app.ilm.uz"]);
const TEKSHIRUVCHILAR = {
endsWith: (o) => o.endsWith("ilm.uz"),
startsWith: (o) => o.startsWith("https://ilm.uz"),
regex: (o) => /https:\/\/.*ilm\.uz/.test(o),
royxat: (o) => RUXSAT.has(o),
};
const ORIGINLAR = [
"https://app.ilm.uz",
"https://yovuzilm.uz",
"https://ilm.uz.yovuz.com",
"http://ilm.uz",
"null",
];
for (const [nom, f] of Object.entries(TEKSHIRUVCHILAR)) {
const natija = ORIGINLAR.map((o) => (f(o) ? "+" : "-")).join(" ");
console.log(nom.padEnd(10), natija);
}Konsolda:
endsWith + + - + -
startsWith - - + - -
regex + + + - -
royxat + - - - -Ustunlar — ORIGINLAR tartibida. Faqat birinchisi bizniki, qolgan to'rttasi hujumchiniki bo'lishi mumkin:
endsWith("ilm.uz")—yovuzilm.uzni o'tkazdi (domen sotib olish oson) vahttp://ni ham.startsWith—ilm.uz.yovuz.comni o'tkazdi: hujumchi o'z domenidailm.uznomli subdomen ochadi.- Regex —
^va$yo'q,.*hamma narsani yutadi: ikki hujum o'tdi (Regex amaliyotda va ReDoS). - Ro'yxat (
Set) — faqat aniq moslik. Kanon usuli.
Beshinchi origin — "null". Brauzer uni sandbox'dagi iframe, file:// sahifa va ba'zi redirect'lardan yuboradi. Hujumchi null origin'li sahifani oson yasaydi. Hech qachon ro'yxatga null qo'shmang. Kanonning CSRF tekshiruvi ham null ni alohida rad etadi.
Ro'yxat dinamik bo'lishi mumkin (bazadan, mijoz domenlari), lekin solishtirish baribir aniq satr bo'lsin. Subdomen kerak bo'lsa — URL bilan ajrating va hostname ni "." + domen bilan tekshiring (2-mashq).
Biz vazifalar-api ni ham sinadik (biz ishga tushirdik). Origin: http://localhost:5173/ (oxirida /) va Origin: null — ikkalasida ham javob 200, lekin Access-Control-Allow-Origin qatori yo'q. cors ro'yxatni aniq solishtiradi — kutilganidek.
6. CORS bajarilishni to'xtatmaydi
6.1 Server javob beradi, brauzer yashiradi
Begona origin va haqiqiy sessiya cookie'si bilan GET (biz ishga tushirdik, sarlavhalar qisqartirilgan):
HTTP/1.1 200 OK
Vary: Origin
Access-Control-Allow-Credentials: true
RateLimit-Policy: 600;w=60
Content-Type: application/json; charset=utf-8
{"versiya":13,"vazifalar":[{"id":1,"matn":"JavaScript kursining 26-qismini boshlash #o'qish"}],"sahifa":{"limit":1,"keyingi":"eyJ2IjpbMSwxXSwiaSI6IltbXSx7fV0ifQ.q31vWhSbd967scFcbk0nvg"}}Server Azizning vazifasini berdi — Allow-Origin yo'q, shuning uchun brauzer uni JS'ga bermaydi. GET uchun bu yetarli: ma'lumot o'zgarmadi, hujumchi o'qiy olmadi. Kanonning brauzer testi aynan buni tekshiradi: begona sahifadan fetch → TypeError, garchi server so'rovni bajargan bo'lsa ham.
6.2 Yozuvchi so'rovni kim to'xtatadi
Oddiy so'rov (masalan, forma yoki text/plain bilan POST) preflight'siz ketadi va server bajaradi. Javobni hujumchi o'qiy olmaydi, lekin unga kerak ham emas — vazifa allaqachon o'chdi. Bu — CSRF (Cross-Site Request Forgery). Himoyasi CORS emas, server tekshiruvi (Tokenni qayerda saqlash va CSRF). vazifalar-api da u bor. Begona origin, haqiqiy cookie, DELETE (biz ishga tushirdik):
HTTP/1.1 403 Forbidden
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Type: application/problem+json; charset=utf-8
{"type":"https://vazifalar-api.example/xatolar/csrf","title":"Ruxsatsiz manba","status":403,"detail":"So'rov boshqa sahifadan kelgan (Origin: https://yovuz.example) — rad etildi","instance":"/v1/vazifalar/2","kod":"csrf","sorovId":"d304d37e-888e-47e6-86f9-89fa9d13f1c1"}Birinchi bo'limdagi 204 endi 403. CSRF tekshiruvi CORS bilan bir ro'yxatdan foydalanadi: ruxsat etilgan origin'lar — o'z origin'imiz va CORS_ORIGINS.
sequenceDiagram
participant H as Begona sahifa
participant B as Brauzer
participant A as vazifalar-api
H->>B: fetch DELETE, credentials include
B->>A: OPTIONS preflight, Origin yovuz
A-->>B: 204, Allow-Origin yo'q
Note over B: DELETE yuborilmaydi
H->>B: forma POST (oddiy so'rov)
B->>A: POST + cookie, Origin yovuz
A-->>B: 403 csrf
Note over A: CSRF tekshiruvi to'xtatdi, CORS emasDiagrammadagi ikki yo'lga qarang. Birinchisida (preflight bilan) CORS haqiqatan himoya qildi — lekin faqat brauzer preflight yuborgani uchun. Ikkinchisida preflight yo'q, va faqat server tekshiruvi qutqardi.
7. Hujumchi nigohi: hujum → himoya → test
| Hujumchi nima qiladi | Natija vazifalar-api da |
Himoya | Qaysi test qotiradi |
|---|---|---|---|
curl bilan begona Origin |
200, lekin cookie'siz — 401 | autentifikatsiya (CORS curl'ga tegishli emas) | auth testlari |
begona sahifadan cookie bilan GET |
brauzer javobni bermaydi | Allow-Origin yo'q |
cors.e2e.ts, 2-test |
* + credentials beradigan server |
brauzer rad etadi | spetsifikatsiya | cors.e2e.ts, 3-test |
begona sahifadan forma POST |
403 csrf |
Origin tekshiruvi | csrf.e2e.ts, csrf.test.ts |
yovuzilm.uz, ilm.uz.yovuz.com |
ruxsat yo'q | aniq ro'yxat | 2-mashq |
sandbox iframe'dan (Origin: null) |
ruxsat yo'q | null ro'yxatda yo'q |
csrf.test.ts |
| CDN keshidagi boshqa origin javobi | kesh ajratiladi | Vary: Origin |
middleware.test.ts |
8. Ko'p uchraydigan xatolar
| Xato | Nega xavfli | To'g'risi |
|---|---|---|
| "CORS — server himoyasi" | so'rov baribir bajariladi | auth + CSRF |
origin: true + credentials: true |
har qanday sayt javobni o'qiydi | aniq ro'yxat |
* + credentials |
brauzer rad etadi | aniq origin |
endsWith, startsWith, langarsiz regex |
hujumchi domeni o'tadi | Set yoki URL.hostname |
ro'yxatda null |
sandbox iframe origin'i | hech qachon |
ro'yxatda oxiri / bilan |
hech qachon mos kelmaydi | URL.origin ga keltirish |
Vary: Origin siz dinamik javob |
kesh boshqa origin'ga beradi | Vary: Origin |
Lax cookie bilan boshqa saytdan |
cookie yuborilmaydi, 401 | bir sayt yoki proxy |
9. Mashqlar
Mashqlar kurs/mashqlar/28/25-cors/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
- Ro'yxatda yo'q origin'dan kelgan
GETni server bajarmaydi. Content-Type: application/jsonbilanPOSTpreflight talab qiladi.credentials: "include"bilan javobdaAccess-Control-Allow-Origin: *bo'lsa, brauzer javobni beradi.https://app.ilm.uzvahttps://api.ilm.uz— bir sayt, lekin ikki origin.Access-Control-Max-Age: 86400— Chromium preflight'ni 24 soat eslab qoladi.
Yechim
- Noto'g'ri — bajaradi va javob qaytaradi; brauzer uni JS'ga bermaydi.
- To'g'ri —
application/json"oddiy" uchta turga kirmaydi. - Noto'g'ri — credentials bilan
*taqiqlangan,TypeError. - To'g'ri — origin xostni to'liq hisobga oladi, sayt esa ro'yxatdan o'tgan domenni.
- Noto'g'ri — Chromium 2 soatgacha qisqartiradi (MDN).
2-mashq (o'rta): xavfsiz origin tekshiruvi
originRuxsat(origin, aniq, domen) funksiyasini yozing: aniq — origin'lar to'plami (Set), domen — subdomenlariga ruxsat beriladigan domen (masalan, "ilm.uz"). Ruxsat: origin aniq da bor; yoki origin https: sxemali, porti sukutdagi va hostname aynan domen yoki "." + domen bilan tugaydi. "null" va yaroqsiz satr — rad (new URL xato tashlasa, false). Oltita origin'da sinang. Ishora: URL.canParse (URL va URLSearchParams) yoki try/catch.
Yechim
function originRuxsat(origin, aniq, domen) {
if (aniq.has(origin)) return true;
if (origin === "null" || !URL.canParse(origin)) return false;
const u = new URL(origin);
// yo'l, oxiridagi "/", katta harf — brauzer yubormaydigan shakl
if (u.origin !== origin) return false;
if (u.protocol !== "https:" || u.port !== "") return false;
return u.hostname === domen || u.hostname.endsWith(`.${domen}`);
}
const ANIQ = new Set(["http://localhost:5173"]);
const SINOV = [
"http://localhost:5173",
"https://app.ilm.uz",
"https://yovuzilm.uz",
"https://ilm.uz.yovuz.com",
"http://app.ilm.uz",
"null",
];
for (const o of SINOV) {
console.log(o.padEnd(26), originRuxsat(o, ANIQ, "ilm.uz"));
}Konsolda:
http://localhost:5173 true
https://app.ilm.uz true
https://yovuzilm.uz false
https://ilm.uz.yovuz.com false
http://app.ilm.uz false
null falseKalit — endsWith ni butun origin satriga emas, ajratilgan hostname ga va nuqta bilan qo'llash: .ilm.uz bilan yovuzilm.uz tugamaydi. u.origin !== origin tekshiruvi brauzer hech qachon yubormaydigan shakllarni (yo'l bilan, katta harf bilan) rad etadi. Eslatma: subdomenga ishonish — o'sha domenning hamma subdomeniga ishonish. Ulardan birida XSS bo'lsa, hujumchi sizning API'ingizni o'qiydi. Shuning uchun kanon aniq ro'yxatni afzal ko'radi.
3-mashq (qiyin): CORS javobini yig'ish
corsSarlavhalar(sorov, sozlama) funksiyasini yozing — cors paketining kichik nusxasi. sorov — { metod, origin, sarlavhalar }, sozlama — { originlar, metodlar, ruxsatSarlavhalar, maxAge }. Qoidalar: har doim Vary: Origin; origin ro'yxatda bo'lsa — Access-Control-Allow-Origin (aynan o'sha origin) va Access-Control-Allow-Credentials: true; OPTIONS bo'lsa — status 204, metodlar va Max-Age; preflight'da so'ralgan sarlavha ruxsat etilmagan bo'lsa — Allow-Origin qo'yilmaydi. Natija — { status, sarlavhalar }. Uchta so'rovni JSON.stringify bilan chiqaring.
Yechim
function corsSarlavhalar(sorov, sozlama) {
const s = { Vary: "Origin" };
const preflight = sorov.metod === "OPTIONS";
let ruxsat = sozlama.originlar.includes(sorov.origin);
if (preflight) {
const sorandi = (sorov.sarlavhalar ?? [])
.map((x) => x.toLowerCase());
const mumkin = sozlama.ruxsatSarlavhalar
.map((x) => x.toLowerCase());
if (!sorandi.every((x) => mumkin.includes(x))) ruxsat = false;
}
if (ruxsat) {
s["Access-Control-Allow-Origin"] = sorov.origin;
s["Access-Control-Allow-Credentials"] = "true";
}
if (!preflight) return { status: 200, sarlavhalar: s };
s["Access-Control-Allow-Methods"] = sozlama.metodlar.join(",");
s["Access-Control-Max-Age"] = String(sozlama.maxAge);
return { status: 204, sarlavhalar: s };
}
const SOZLAMA = {
originlar: ["http://localhost:5173"],
metodlar: ["GET", "POST", "PATCH", "DELETE"],
ruxsatSarlavhalar: ["Content-Type", "If-Match"],
maxAge: 600,
};
const F = "http://localhost:5173";
const SOROVLAR = [
{ metod: "GET", origin: F },
{ metod: "OPTIONS", origin: F, sarlavhalar: ["content-type"] },
{ metod: "OPTIONS", origin: F, sarlavhalar: ["x-admin"] },
];
for (const q of SOROVLAR) {
console.log(JSON.stringify(corsSarlavhalar(q, SOZLAMA)));
}Konsolda:
{"status":200,"sarlavhalar":{"Vary":"Origin","Access-Control-Allow-Origin":"http://localhost:5173","Access-Control-Allow-Credentials":"true"}}
{"status":204,"sarlavhalar":{"Vary":"Origin","Access-Control-Allow-Origin":"http://localhost:5173","Access-Control-Allow-Credentials":"true","Access-Control-Allow-Methods":"GET,POST,PATCH,DELETE","Access-Control-Max-Age":"600"}}
{"status":204,"sarlavhalar":{"Vary":"Origin","Access-Control-Allow-Methods":"GET,POST,PATCH,DELETE","Access-Control-Max-Age":"600"}}Uchinchi qator — haqiqiy cors paketining xulqiga o'xshash: status 204, lekin Allow-Origin yo'q, va brauzer asosiy so'rovni yubormaydi. Vary: Origin har doim — ruxsat berilmagan javob ham keshda boshqa origin'nikidan ajralib tursin. Sarlavha nomlarini kichik harfga keltirdik: brauzer Access-Control-Request-Headers da ularni kichik harf bilan yuboradi.
10. Loyiha qadami: CORS credentials va brauzer testlari
10.1 src/ilova.ts
25-qismdagi cors sozlamasiga ikki o'zgarish: credentials: true va allowedHeaders ga Authorization (API kaliti uchun, API kalitlar va servislararo autentifikatsiya). Telefon uchun bo'lingan, izohlar qisqartirilgan (biz tekshirdik: tip, lint va testlar o'tdi):
// CORS: faqat ro'yxatdagi origin'lar (aniq satr — config.ts da
// normallangan). credentials: true — cookie sessiyasi bilan boshqa
// origin'dagi frontend so'rov yubora olsin (fetch credentials:
// "include"). DIQQAT: credentials bilan "*" mumkin emas — brauzer
// javobni rad etadi (e2e). Preflight (OPTIONS) shu yerda javob
// oladi; Authorization — API kaliti uchun.
app.use(
cors({
origin: [...corsOriginlar],
credentials: true,
allowedHeaders: [
"Content-Type", "Authorization", "Idempotency-Key",
"X-Request-Id", "If-Match", "If-None-Match",
],
exposedHeaders: [
"Location", "X-Request-Id", "Idempotency-Replayed",
"ETag", "Link", "Deprecation", "Sunset",
],
maxAge: 600,
}),
);Bu blok helmet va Permissions-Policy middleware'laridan keyin, CSRF va routerlardan oldin turadi. Bitta kichik kuzatuv: credentials: true bilan cors paketi Access-Control-Allow-Credentials: true ni har javobga qo'yadi, hatto Origin sarlavhasi bo'lmasa ham (oldingi darsdagi curl chiqishiga qarang). Bu zararsiz — Allow-Origin siz u hech narsa ochmaydi.
10.2 Birlik testi
test/middleware.test.ts dagi CORS testiga uchta tekshiruv qo'shildi — preflight javobida:
const s = pre.sarlavhalar;
assert.equal(s["access-control-allow-credentials"], "true");
assert.match(String(s["access-control-allow-headers"]),
/Authorization/);
assert.match(String(s["vary"]), /Origin/);Kanonda s o'rniga pre.sarlavhalar to'liq yozilgan — telefon uchun qisqartirdik.
10.3 Brauzer testlari: e2e/cors.e2e.ts
Birlik testi sarlavhalarni tekshiradi, lekin brauzer ularga qanday amal qilishini ko'rsatmaydi. Kanon Playwright bilan haqiqiy Chromium'da to'rtta test yozdi. Frontend va "begona sayt" — kichik HTML serverlar, http://localhost:<port> da (API bilan bir sayt, boshqa origin). Ichida fetch qiladigan yordamchi va uchta CORS testi (telefon uchun bo'lingan, tip va lint o'tdi):
const yubor = (
p: import("playwright").Page,
url: string,
credentials: RequestCredentials,
) => p.evaluate(async ([u, c]) => {
try {
const r = await fetch(u, { credentials: c });
return `${String(r.status)} ${await r.text()}`.slice(0, 80);
} catch (x) {
return `xato: ${String(x)}`;
}
}, [url, credentials] as const);
test("ro'yxatdagi origin + credentials: include — 200 va javob " +
"o'qiladi; credentials'siz — cookie yo'q, 401", async () => {
const ctx = await kirganKontekst();
const p = await ctx.newPage();
await p.goto(`${FRONT}/`);
const men = `${a.asos}/v1/auth/men`;
const javob = await yubor(p, men, "include");
assert.match(javob, /^200 \{"foydalanuvchi"/);
assert.match(await yubor(p, men, "omit"), /^401/);
await ctx.close();
});
test("ro'yxatda yo'q origin — brauzer javobni bermaydi " +
"(TypeError), garchi server so'rovni bajargan bo'lsa ham",
async () => {
const ctx = await kirganKontekst();
const p = await ctx.newPage();
await p.goto(`http://localhost:${String(begona.port)}/`);
const men = `${a.asos}/v1/auth/men`;
assert.match(await yubor(p, men, "include"), /^xato: TypeError/);
await ctx.close();
});
test("'*' + Access-Control-Allow-Credentials: true — brauzer " +
"rad etadi (spetsifikatsiya); '*' credentials'siz — ishlaydi",
async () => {
const yomon = createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
const cookie = JSON.stringify(req.headers.cookie ?? null);
res.end(`{"cookie":${cookie}}`);
});
await new Promise<void>((r) => yomon.listen(0, "127.0.0.1", r));
const port = (yomon.address() as AddressInfo).port;
const url = `http://localhost:${String(port)}/`;
const ctx = await b.newContext();
const p = await ctx.newPage();
await p.goto(`${FRONT}/`);
assert.match(await yubor(p, url, "include"), /^xato: TypeError/);
assert.match(await yubor(p, url, "omit"), /^200/);
await ctx.close();
yomon.close();
});kirganKontekst() — brauzer kontekstiga sessiya beradigan yordamchi: API'ning o'ziga parol bilan kiradi va cookie kontekstda qoladi. Uchinchi testda * + credentials beradigan ataylab yomon server — biz o'z API'imizda bunday sozlamani hech qachon yozmaymiz, lekin brauzerning rad etishini o'z ko'zimiz bilan ko'rishimiz kerak. To'rtinchi test (bu yerda ko'rsatilmagan) — hsts: true bilan HSTS sarlavhasi, oldingi darsdagi va'da.
p.evaluate ichidagi kod brauzerda ishlaydi — xato TypeError ni aynan brauzer qaytaradi. String(x) uni satrga aylantiradi, test esa faqat boshini (xato: TypeError) solishtiradi: Chromium xabarining qolgan qismi versiyaga qarab o'zgarishi mumkin.
10.4 Halol eslatma: tasodifiy yiqiladigan test
Holatning birinchi to'liq tekshiruvida bitta birlik testi ham yiqildi — CORS'ga aloqasi yo'q, lekin buni yashirmaymiz (kanon tekshiruvi):
✖ kalit auth yo'llarini boshqara olmaydi: kalitlar, /men, admin — 401; yaroqsiz kalit — 401 + WWW-Authenticate Bearer (424.6569ms)Sababi — API kaliti testida (oldingi darslar). "Yaroqsiz kalit" yasash uchun test kalitning oxirgi belgisini A ga almashtirardi. Kalit tasodifiy: oxirgi belgi o'zi A bo'lsa (64 tadan 1 holat) — kalit o'zgarmasdi va test yiqilardi. Bunday test flaky (beqaror) deyiladi. Tuzatish — albatta boshqa belgiga almashtirish:
// oxirgi belgini ALBATTA boshqasiga almashtirish (1/64 da 'A'
// bo'lsa kalit o'zgarmasdi)
const oxiri = k.kalit.endsWith("A") ? "B" : "A";
for (const x of [
`${k.kalit.slice(0, -1)}${oxiri}`,
"vz_aaaaaaaa_" + "A".repeat(43),
"boshqa-token",
]) {Kanonda bu bitta uzun qator, oxiri o'zgaruvchisini telefon uchun ajratdik. Saboq: test tasodifiy qiymat ishlatsa, "o'zgartirdim" deb taxmin qilmang — o'zgarganini ta'minlang.
10.5 Tekshiruv va commit
Holatning yakuniy tekshiruvi (kanon, 2026-10-28, PostgreSQL 18.6) toza o'tdi. Tip va lint xatosiz, testlar 210 (209 + 1 o'tkazilgan), Testcontainers 10/10, brauzer testlari 10/10 (CSRF 4, passkey 2, CORS 4), migratsiyalar va schema.prisma orasida farq yo'q.
--- e2e (haqiqiy brauzer, Chromium headless shell): ℹ tests 10 ℹ pass 10 ℹ fail 0 ℹ skipped 0Brauzer testlari uchun sizda Chromium kerak: npx playwright install chromium (Playwright I: e2e testlar asoslari). Ishga tushirish — npm run e2e.
Ikki darsning ishini commit qiling:
M="feat(api): API uchun qat'iy xavfsizlik sarlavhalari (CSP 'none',"
M+=" DENY, CORP same-site, Permissions-Policy, HSTS 1 yil)"
M+=" va CORS credentials; e2e: CORS brauzerda"
git add src/ilova.ts test e2e
git commit -m "$M"Xabarda apostrof bor (qat'iy), shuning uchun qo'sh tirnoq.
11. Real ishda
- "CORS xatosi" — frontend dasturchilarning eng ko'p savollaridan. Ko'pincha sababi CORS emas: server 500 qaytardi va xato javobida CORS sarlavhalari yo'q edi. Shuning uchun
corsmiddleware routerlardan oldin turadi — xato javobida ham sarlavhalar bo'ladi. - Katta tizimlarda frontend va API bir saytga qo'yiladi (
ilm.uzvaapi.ilm.uz) yoki bitta origin'da proxy orqali beriladi — shunda cookie siyosati sodda va CORS'ning murakkab qismi kerak bo'lmaydi. - Ochiq API (kalit bilan, cookie'siz) uchun
*normal: masalan, ommaviy ma'lumot yoki shrift beradigan xizmatlar. - Intervyuda: "CORS nimadan himoya qiladi va nimadan qilmaydi?", "preflight qachon bo'ladi?", "nega credentials bilan
*ishlamaydi?", "CORS va CSRF farqi?".
Xulosa
- CORS — same-origin devoridagi eshik: brauzer javobni JS'ga faqat server ruxsat bersa beradi. So'rov baribir bajariladi.
- Origin — sxema + xost + port; sayt — kengroq.
SameSitecookie saytga qaraydi. - JSON,
PATCH,DELETE,Authorization— preflight.text/plainbilanPOST— preflight'siz. - Cookie bilan:
credentials: "include"+Allow-Credentials: true+ aniq origin;*va "aks ettirish" — yo'q. - Origin tekshiruvi — aniq
Set;endsWith,startsWith, langarsiz regex vanull— teshik. - Yozishdan himoya — CSRF tekshiruvi (403), CORS emas.
- Brauzer xulqini faqat brauzer testi ko'rsatadi — kanon to'rtta Playwright testi bilan qotirdi.
Keyingi dars: Rate limiting — 27-qism API'si 300 ta ketma-ket so'rovga 300 marta 200 bergan edi. Login, 2FA va SMS yo'llariga tezlik cheklovini qo'yamiz va cheklagich ishlamay qolsa nima qilishni hal qilamiz.
Manbalar
- MDN: Cross-Origin Resource Sharing (CORS) — oddiy so'rov shartlari, credentials va
*qoidasi, uchinchi tomon cookie'lari: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS (2026-10-28 da o'qildi) - MDN:
Access-Control-Max-Age— brauzer chegaralari (Chromium 7 200 s, Firefox 86 400 s, sukut 5 s): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age (2026-10-28 da o'qildi) - WHATWG Fetch Standard, CORS protokoli: https://fetch.spec.whatwg.org/#http-cors-protocol
corspaketi (2.8.6): https://github.com/expressjs/cors- OWASP: Testing Cross Origin Resource Sharing: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing
- MDN:
Set-CookievaSameSite: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie - Kanon (IlmHamroh, 2026-10-27/28): hujum yuzasi tajribasi (27-qism yakuni, begona origin), holat 25 tekshiruvlari (e2e 10/10, flaky test),
config.ts,e2e/cors.e2e.ts. Darsdagi brauzer misollari va jonlicurl(Node 24.21.0, PostgreSQL 17.6) — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!