IlmHamroh
JavaScript Full-stack/28-qism. Autentifikatsiya va backend xavfsizligi24/31-dars18 daqiqa
Mundarija (32)

Helmet va xavfsizlik sarlavhalari: API uchun qat'iy CSP, HSTS, clickjacking va Permissions-Policy

Qisqacha: Xavfsizlik sarlavhalari (security headers) — server brauzerga beradigan ko'rsatmalar: "meni iframe'ga qo'yma", "Content-Type'ni taxmin qilma", "faqat HTTPS orqali kel". Ularni server emas, brauzer bajaradi. helmet() sukuti brauzer sahifasi uchun mo'ljallangan; JSON qaytaradigan API uchun undan ham qattiqroq siyosat kerak: default-src 'none', frame-ancestors 'none', X-Frame-Options: DENY. Bu darsda vazifalar-api sarlavhalarini API uchun toraytiramiz va har birini test bilan qotiramiz.

Bu darsda

  • helmet() sukutda qo'yadigan sarlavhalarni o'qiysiz va qaysi biri kimga kerakligini ajratasiz.
  • CSP direktivalari qachon default-src ga "qaytishini", qachon qaytmasligini ko'rasiz.
  • Clickjacking'dan frame-ancestors va X-Frame-Options bilan himoyalanasiz.
  • HSTS'ni qachon yoqish va qachon yoqmaslikni tushuntirasiz.
  • vazifalar-api ga API uchun qat'iy sarlavhalarni va ularning testini qo'shasiz.

Oldin bilishingiz kerak: Mashhur middleware'lar (helmet va cors birinchi marta), Brauzer xavfsizlik modeli (CSP, COOP, Permissions-Policy), XSS va DOM xavfsizligi, HTTPS, TLS va HTTP/2, OWASP Top 10:2025 xaritasi.

25-qismda vazifalar-api ga bitta qator qo'shgan edik: app.use(helmet()). Sarlavhalar paydo bo'ldi va biz shu bilan qoldik. 13-qismda esa CSP'ni sahifa nuqtai nazaridan o'rgandik. Bugun savol boshqacha: JSON qaytaradigan API uchun helmet sukuti to'g'rimi? Javob: yo'q, u API uchun keragidan yumshoq.

1. Nega bu kerak?

1.1 Sarlavhani kim bajaradi

Xavfsizlik sarlavhasi — eshikka yopishtirilgan yozuvga o'xshaydi: "Kirish faqat xodimlar uchun". Yozuvning o'zi hech kimni to'xtatmaydi. Uni hurmat qiladigan qorovul — brauzer. curl, Postman yoki hujumchining skripti bu sarlavhalarga umuman qaramaydi.

Demak sarlavhalar serverni emas, foydalanuvchini himoya qiladi: uning brauzerida sizning javobingiz yomon maqsadda ishlatilmasin. Bu himoya qatlamlaridan biri (Xavfsizlik tamoyillari — chuqur himoya). Uning o'rnini parametrli so'rov yoki ruxsat tekshiruvi bosa olmaydi va aksincha.

1.2 API'ni ham brauzer ochadi

"Bizda sahifa yo'q, faqat JSON" — keng tarqalgan fikr. Lekin brauzer API javobini bir necha yo'l bilan olishi mumkin:

  • hujumchi foydalanuvchiga https://api…/v1/vazifalar?q=<script>… havolasini yuboradi va brauzer javobni sahifa sifatida ochadi;
  • boshqa sayt API manzilini <iframe>, <img> yoki <script> ichiga qo'yadi;
  • noto'g'ri Content-Type bilan kelgan javobni brauzer "taxmin qilib" HTML deb o'qiydi.

Har holatda sarlavha brauzerga aniq javob beradi: "bu sahifa emas, hech narsa yuklama, hech qayerga qo'yma".

flowchart LR
  H[Hujumchi sahifasi] -->|iframe| A[API javobi]
  H -->|img yoki script| A
  F[Foydalanuvchi] -->|havola orqali ochadi| A
  A --> B{Brauzer sarlavhalarni o'qiydi}
  B -->|frame-ancestors none| X1[iframe bloklandi]
  B -->|CORP same-site| X2[begona sayt o'qiy olmaydi]
  B -->|default-src none| X3[hech narsa yuklanmaydi]

Diagrammaning o'ng tomoniga qarang: uchala yo'lni ham brauzer to'xtatadi, server faqat ko'rsatma beradi. OWASP Top 10:2025 da bunday sozlamalar A02 Security Misconfiguration toifasiga kiradi.

2. helmet() sukuti: nima bor

2.1 27-qism oxiridagi sarlavhalar

28-qism boshida kanon vazifalar-api (27-qism yakuni) ni hujumchi ko'zi bilan tekshirgan edi. Har javobda shu sarlavhalar bor edi (kanon tajribasi, curl -i, sarlavhalarning bir qismi). Birinchi qatorga — CSP uzunligiga va X-Frame-Options qiymatiga qarang:

text
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0

Bu — helmet 8.3.0 sukuti. Strict-Transport-Security (HSTS) yo'q, chunki 25-qismda uni ataylab HSTS sozlamasiga bog'lagan edik — lokal http da yoqilmaydi. X-Powered-By: Express ham yo'q — uni app.disable("x-powered-by") olib tashlaydi.

2.2 Har biri nima uchun

Sarlavha Brauzerga ko'rsatma API uchun
Content-Security-Policy qayerdan resurs yuklash mumkin toraytiramiz
Cross-Origin-Opener-Policy begona oyna window.opener orqali tegmasin qoladi
Cross-Origin-Resource-Policy qaysi saytlar <img>/<script> bilan ola oladi same-site ga
Origin-Agent-Cluster sahifani alohida jarayon guruhiga ajratish qoladi
Referrer-Policy havola bosilganda manzil yuborilmasin qoladi
X-Content-Type-Options Content-Type ni taxmin qilma eng muhimlaridan
X-DNS-Prefetch-Control DNS'ni oldindan so'rama qoladi
X-Download-Options eski IE uchun zararsiz
X-Frame-Options iframe'ga qo'yish DENY ga
X-Permitted-Cross-Domain-Policies eski Flash/PDF siyosati zararsiz
X-XSS-Protection: 0 eski XSS filtrini o'chir qoladi

Ko'pi oldingi darslardan tanish: nosniff va X-XSS-Protection: 0 — Mashhur middleware'lar darsida, COOP va Permissions-Policy — Brauzer xavfsizlik modeli darsida. Bugun uchtasiga chuqur kiramiz: CSP, frame-ancestors va HSTS.

2.3 Sukut sahifa uchun yozilgan

Sukutdagi CSP'ni o'qing: style-src 'self' https: 'unsafe-inline', img-src 'self' data:, font-src 'self' https: data:. Bu — oddiy veb-sahifa uchun murosa: o'z skriptlari, Google Fonts, inline stil. Helmet hujjati ham buni aytadi: sukut — boshlang'ich nuqta, uni ilovangizga moslashtirish kerak.

JSON API esa hech qanday rasm, shrift yoki stil yuklamaydi. Unga yuklash huquqini berish — keraksiz eshik. "Eng kam huquq" tamoyili sarlavhalarga ham tegishli: kerak bo'lmagan narsa — 'none'.

3. CSP: API uchun default-src 'none'

3.1 Qaysi direktiva qayerga "qaytadi"

CSP'da default-src — zaxira qiymat. script-src, style-src, img-src, connect-src kabi fetch direktivalari yozilmagan bo'lsa, brauzer default-src ni oladi. Lekin hamma direktiva shunday emas: frame-ancestors, base-uri va form-action default-src ga qaytmaydi. Ularni yozmasangiz — cheklov yo'q.

Buni kichik funksiya bilan ko'raylik. U CSP satrini bo'laklaydi va direktiva uchun amaldagi qiymatni topadi:

js
const SUKUT =
  "default-src 'self';base-uri 'self';font-src 'self' https: data:;" +
  "form-action 'self';frame-ancestors 'self';img-src 'self' data:;" +
  "object-src 'none';script-src 'self';script-src-attr 'none';" +
  "style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests";
const API =
  "default-src 'none';base-uri 'none';form-action 'none';" +
  "frame-ancestors 'none'";
const FAQAT_DEFAULT = "default-src 'none'";

function oqi(csp) {
  const m = new Map();
  for (const qism of csp.split(";")) {
    const [nom, ...qiymat] = qism.trim().split(/\s+/);
    if (nom) m.set(nom, qiymat.join(" "));
  }
  return m;
}

// fetch direktivalari default-src ga "qaytadi", qolganlari — yo'q
const FETCH = ["connect-src", "img-src", "script-src", "style-src"];

function amaldagi(csp, d) {
  if (csp.has(d)) return csp.get(d);
  if (FETCH.includes(d) && csp.has("default-src")) {
    return `${csp.get("default-src")} (default-src dan)`;
  }
  return "CHEKLOV YO'Q";
}

for (const [nom, matn] of [["sukut", SUKUT], ["api", API],
  ["faqat default", FAQAT_DEFAULT]]) {
  const csp = oqi(matn);
  console.log(`--- ${nom}: ${csp.size} ta direktiva`);
  for (const d of ["connect-src", "style-src", "frame-ancestors"]) {
    console.log(d.padEnd(16), amaldagi(csp, d));
  }
}

Konsolda:

text
--- sukut: 11 ta direktiva
connect-src      'self' (default-src dan)
style-src        'self' https: 'unsafe-inline'
frame-ancestors  'self'
--- api: 4 ta direktiva
connect-src      'none' (default-src dan)
style-src        'none' (default-src dan)
frame-ancestors  'none'
--- faqat default: 1 ta direktiva
connect-src      'none' (default-src dan)
style-src        'none' (default-src dan)
frame-ancestors  CHEKLOV YO'Q

Uchinchi guruhga qarang. default-src 'none' "hamma narsani yopdi" degan tuyg'u beradi, lekin iframe himoyasi yo'q. Shuning uchun vazifalar-api siyosatida to'rtta direktiva bor: default-src 'none' + qaytmaydigan uchtasi — base-uri, form-action, frame-ancestors. Funksiyamiz soddalashtirilgan: haqiqiy brauzerda script-src-elem avval script-src ga, keyin default-src ga qaytadi, lekin g'oya shu.

3.2 Bu CSP nimani himoya qiladi

Siz o'ylashingiz mumkin: "JSON'da skript yo'q, CSP nimaga kerak?". Ikki holat:

  1. Xato javobi yoki qidiruv natijasi foydalanuvchi matnini qaytaradi. Agar biror sabab bilan javob text/html bo'lib ketsa (kod xatosi, proxy), <script> ishga tushishi mumkin. default-src 'none' bunda ham skriptni bloklaydi.
  2. API'ning o'z HTML sahifasi bor — public/index.html (qisqa yo'riqnoma). U skriptsiz, shuning uchun 'none' bilan ham to'g'ri ko'rinadi.

Muhim noto'g'ri tushuncha: API'ning CSP'si frontend'ni himoya qilmaydi. CSP faqat o'sha javob sahifa sifatida ochilganda ishlaydi. vazifalar frontend'ining CSP'sini frontend'ni beradigan server (Vite, nginx) qo'yadi — buni 13-qismda yozgansiz.

3.3 Kanon topilmasi: CSP o'z sahifasini ham to'sdi

Yangi siyosat bilan birinchi to'liq tekshiruvda brauzer testlaridan biri yiqildi (kanon tekshiruvi, holat 25):

text
--- e2e (haqiqiy brauzer, Chromium headless shell): ℹ tests 10 ℹ pass 9 ℹ fail 1 ℹ skipped 0
✖ o'z sahifasidan (bir origin) fetch POST JSON — ishlaydi (201) (708.7692ms)

Test API'ning / sahifasini (o'sha public/index.html) ochib, undan fetch("/v1/vazifalar") qilardi. Endi bu sahifada connect-src yo'q, demak default-src 'none' ishlaydi — brauzer o'z origin'iga ham fetch ni taqiqladi.

Bu xato emas, kutilgan xulq: API sahifa bermaydi, demak uning sahifasidan so'rov ham kerak emas. Kanon testni ikki qismga ajratdi: endi test CSP'ning fetch ni to'sishini ham tekshiradi (TypeError kutadi), "frontend" sahifasi esa test serverida API middleware'laridan oldin beriladi (/frontend.html). Prodakshnda frontend'ni nginx beradi.

Saboq: qat'iy sarlavha qo'yganda brauzer testi shart. curl bu yiqilishni hech qachon ko'rsatmas edi.

Tekshirib ko'ring: Hamkasbingiz "frontend'imizda XSS bo'lmasligi uchun API'ga default-src 'none' qo'ydim" dedi. Uning fikrida nima noto'g'ri?

Javob

CSP faqat uni olgan hujjatga (sahifaga) tegishli. Frontend sahifasi frontend serveridan keladi va o'z CSP'siga ega. API JSON javobidagi CSP frontend sahifasining skriptlariga ta'sir qilmaydi — u faqat API javobi brauzerda alohida sahifa bo'lib ochilgan holatni himoya qiladi. Frontend'ning XSS himoyasi — frontend sahifasining CSP'si va xavfsiz render (XSS va DOM xavfsizligi).

4. Clickjacking: frame-ancestors va X-Frame-Options

4.1 Hujum

Clickjacking — hujumchi sizning sahifangizni o'z saytida ko'rinmas <iframe> ichiga qo'yadi va ustiga "Sovg'ani olish" tugmasini chizadi. Foydalanuvchi tugmani bosaman deb, aslida sizning sahifangizdagi "Hisobni o'chirish" ni bosadi. Cookie brauzerda bor — so'rov uning nomidan ketadi.

4.2 Ikki sarlavha

Sarlavha Qiymat Eslatma
Content-Security-Policy: frame-ancestors 'none' hech kim iframe'ga qo'ya olmaydi zamonaviy standart
X-Frame-Options: DENY xuddi shu eski brauzerlar uchun

Helmet sukuti SAMEORIGIN va frame-ancestors 'self' — o'z saytingiz iframe'ga qo'ya oladi. API uchun bunga ham ehtiyoj yo'q, shuning uchun ikkalasi ham 'none'/DENY. Ikkala sarlavha bor bo'lsa, zamonaviy brauzer CSP'dagi frame-ancestors ga amal qiladi. Eski sarlavha esa faqat zaxira.

Eslatma: frame-ancestors ni <meta http-equiv> orqali qo'yib bo'lmaydi — faqat HTTP sarlavhasi (Brauzer xavfsizlik modeli). Bu yana bir sabab: himoya serverda.

5. HSTS: faqat HTTPS orqasida

5.1 Nima qiladi

Strict-Transport-Security: max-age=31536000; includeSubDomains — brauzerga: "keyingi bir yil davomida bu domenga va uning hamma subdomeniga faqat HTTPS orqali kel". Foydalanuvchi http:// ni yozsa ham, brauzer o'zi https:// ga o'tadi. Bu SSL stripping hujumini (ochiq Wi-Fi'da birinchi http so'rovni ushlab, HTTPS'ga o'tkazmaslik) yopadi.

5.2 Nega kanon uni sukutda o'chirdi

  1. RFC 6797 ga ko'ra brauzer http orqali kelgan HSTS sarlavhasini e'tiborsiz qoldiradi. Lokal http://127.0.0.1 da u foydasiz.
  2. Prodakshnda Node odatda HTTPS'ni o'zi emas, oldidagi proxy (nginx) orqali ko'radi (Reverse proxy orqasida Node). Shuning uchun "HTTPS bormi?" degan qarorni kod emas, sozlama beradi: HSTS=true.
  3. includeSubDomains — hamma subdomen HTTPS'da bo'lishi shart. Eski http da qolgan bitta subdomen (masalan, ichki panel) bir yilga ochilmay qoladi.

Preload — domenni brauzerlarning ichki ro'yxatiga yozdirish (hstspreload.org). Shunda birinchi tashrifdan oldin ham faqat HTTPS. Lekin ro'yxatdan chiqish oylab davom etadi. Kanon buni kodga qo'ymadi — bu domen egasining qarori.

6. Qolgan uchtasi: CORP, Referrer va Permissions-Policy

Cross-Origin-Resource-Policy: same-site. Sukut same-origin edi. Bizning frontend API bilan bir saytda turadi: Vite proxy yoki nginx orqali (keyingi darsda ko'ramiz). Lekin har doim ham bir origin'da emas. same-site — bir saytdagi boshqa origin <img> bilan ola oladi, begona sayt — yo'q. 25-qismda ogohlantirgan edik: rasm beradigan API uchun bu qiymat cross-origin bo'lishi kerak bo'lardi. vazifalar-api rasm bermaydi.

Referrer-Policy: no-referrer. Havola bosilganda manzil (unda kursor, qidiruv so'zi bo'lishi mumkin) boshqa saytga ketmaydi. Sukutda bor, qoladi.

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(). Bo'sh qavslar — "hech kimga ruxsat yo'q". Helmet bu sarlavhani qo'ymaydi, shuning uchun kanon uni o'zining kichik middleware'i bilan qo'shdi.

7. Hujumchi nigohi

Hujumchi nima qiladi Qaysi sarlavha to'xtatadi Qachon ishlamaydi Test
API sahifasini iframe'ga qo'yadi frame-ancestors 'none', DENY default-src ga ishonib qolsangiz aniq CSP satri, DENY (404 da ham)
javobga HTML kiritib, sahifa sifatida ochtiradi nosniff, default-src 'none' Content-Type noto'g'ri bo'lsa nosniff; e2e: fetch to'siladi
ochiq Wi-Fi'da birinchi http ni ushlaydi HSTS birinchi tashrifda (preload'siz) hsts: true da aniq qiymat
o'z saytida <img src="api…"> CORP same-site cross-origin qo'yilgan bo'lsa same-site qiymati
X-Powered-By dan freymvork versiyasini biladi app.disable — sarlavha yo'q

Oxirgi qatorni oshirib baholamang: freymvorkni yashirish hujumchini biroz sekinlashtiradi, xolos. Bu "noaniqlik orqali xavfsizlik" emas, shunchaki keraksiz ma'lumotni bermaslik.

8. Ko'p uchraydigan xatolar

Xato Nega xavfli To'g'risi
"helmet qo'ydim — tayyor" sukut sahifa uchun, API uchun yumshoq siyosatni ilovaga moslash
faqat default-src 'none' frame-ancestors qaytmaydi to'rtta direktiva
HSTS'ni http da yoqish brauzer e'tiborsiz qoldiradi, chalg'itadi faqat HTTPS proxy orqasida
includeSubDomains ni o'ylamay http subdomen ochilmay qoladi subdomenlarni tekshirib
API CSP'si frontend'ni himoya qiladi deb o'ylash CSP faqat o'z hujjatiga frontend o'z CSP'si bilan
xato javobida sarlavha yo'q 404 sahifasini iframe'ga qo'yish mumkin helmet eng boshda
sarlavhani faqat curl bilan tekshirish brauzer xulqi ko'rinmaydi brauzer testi

9. Mashqlar

Mashqlar kurs/mashqlar/28/24-helmet/ da.

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

  1. Xavfsizlik sarlavhalari curl bilan yuborilgan hujum so'rovini ham to'xtatadi.
  2. default-src 'none' bo'lsa, frame-ancestors yozilmasa ham iframe taqiqlangan.
  3. http orqali kelgan HSTS sarlavhasini brauzer e'tiborsiz qoldiradi.
  4. API javobidagi CSP frontend sahifasidagi XSS'dan himoya qiladi.
  5. X-XSS-Protection: 0 — eski filtrni ataylab o'chirish.
Yechim
  1. Noto'g'ri — sarlavhalarga faqat brauzer amal qiladi.
  2. Noto'g'ri — frame-ancestors default-src ga qaytmaydi (yuqoridagi "faqat default" natijasi).
  3. To'g'ri — RFC 6797 shuni talab qiladi.
  4. Noto'g'ri — CSP faqat uni olgan hujjatga tegishli.
  5. To'g'ri — o'sha filtrning o'zi zaiflik manbai bo'lgan.

2-mashq (o'rta): sarlavha tekshiruvchisi

sarlavhaTekshir(sarlavhalar) funksiyasini yozing. U sarlavhalar obyektini oladi (nomlar har xil registrda bo'lishi mumkin) va muammolarni ; bilan ulab qaytaradi, muammo bo'lmasa — "toza". Tekshiruvlar: X-Content-Type-Options aynan nosniff; CSP bor; CSP'da frame-ancestors yo'q bo'lsa X-Frame-Options: DENY bo'lsin; X-Powered-By bo'lmasin. Uchta to'plamda sinang: oddiy Express, faqat default-src 'none' + nosniff, kanon. Ishora: nomlarni Object.entries va toLowerCase bilan kichik harfga keltiring (Object metodlarining to'liq sharhi).

Yechim
js
function sarlavhaTekshir(s) {
  const h = Object.fromEntries(
    Object.entries(s).map(([k, v]) => [k.toLowerCase(), v]),
  );
  const xatolar = [];
  if (h["x-content-type-options"] !== "nosniff") {
    xatolar.push("nosniff yo'q");
  }
  const csp = h["content-security-policy"] ?? "";
  if (csp === "") xatolar.push("CSP yo'q");
  const deny = h["x-frame-options"] === "DENY";
  if (!csp.includes("frame-ancestors") && !deny) {
    xatolar.push("iframe himoyasi yo'q");
  }
  if ("x-powered-by" in h) {
    xatolar.push(`freymvork oshkor: ${h["x-powered-by"]}`);
  }
  return xatolar.length === 0 ? "toza" : xatolar.join("; ");
}

const ODDIY_EXPRESS = {
  "X-Powered-By": "Express",
  "Content-Type": "application/json; charset=utf-8",
};
const FAQAT_DEFAULT = {
  "Content-Security-Policy": "default-src 'none'",
  "X-Content-Type-Options": "nosniff",
};
const KANON = {
  "Content-Security-Policy":
    "default-src 'none';base-uri 'none';form-action 'none';" +
    "frame-ancestors 'none'",
  "X-Content-Type-Options": "nosniff",
  "X-Frame-Options": "DENY",
};

console.log(sarlavhaTekshir(ODDIY_EXPRESS));
console.log(sarlavhaTekshir(FAQAT_DEFAULT));
console.log(sarlavhaTekshir(KANON));

Konsolda:

text
nosniff yo'q; CSP yo'q; iframe himoyasi yo'q; freymvork oshkor: Express
iframe himoyasi yo'q
toza

Ikkinchi qator — darsdagi asosiy tuzoq: CSP bor, nosniff bor, lekin iframe himoyasi yo'q. HTTP sarlavha nomlari registrga sezgir emas, shuning uchun avval hammasini kichik harfga keltirdik.

3-mashq (qiyin): CSP yig'uvchi

cspYoz(direktivalar) funksiyasini yozing: obyektdan ({ "default-src": ["'none'"] }) CSP satrini yig'adi. Qoidalar: none, self, unsafe-inline, unsafe-eval tirnoqsiz yozilsa — xato; * — xato; script-src da 'unsafe-inline' — xato; frame-ancestors berilmasa — oxiriga frame-ancestors 'none' qo'shiladi. Xato bo'lsa { xatolar }, bo'lmasa { csp } qaytsin. Uchta to'plamda sinab, natijani satrma-satr chiqaring.

Yechim
js
const KALIT_SOZLAR = ["none", "self", "unsafe-inline", "unsafe-eval"];

function cspYoz(direktivalar) {
  const xatolar = [];
  const qismlar = [];
  for (const [nom, qiymatlar] of Object.entries(direktivalar)) {
    for (const q of qiymatlar) {
      if (KALIT_SOZLAR.includes(q)) {
        xatolar.push(`${nom}: ${q} tirnoqsiz — '${q}' bo'lsin`);
      }
      if (q === "*") xatolar.push(`${nom}: * — hamma joyga ruxsat`);
      if (nom === "script-src" && q === "'unsafe-inline'") {
        xatolar.push("script-src: 'unsafe-inline' — XSS eshigi");
      }
    }
    qismlar.push([nom, ...qiymatlar].join(" "));
  }
  if (!("frame-ancestors" in direktivalar)) {
    qismlar.push("frame-ancestors 'none'");
  }
  if (xatolar.length > 0) return { xatolar };
  return { csp: qismlar.join(";") };
}

function chiqar(n) {
  if (n.csp) console.log(n.csp);
  else for (const x of n.xatolar) console.log("XATO:", x);
}

chiqar(cspYoz({ "default-src": ["'none'"] }));
chiqar(cspYoz({
  "default-src": ["'self'"],
  "img-src": ["'self'", "data:"],
  "frame-ancestors": ["'self'"],
}));
chiqar(cspYoz({
  "default-src": ["none"],
  "script-src": ["'self'", "'unsafe-inline'"],
  "connect-src": ["*"],
}));

Konsolda:

text
default-src 'none';frame-ancestors 'none'
default-src 'self';img-src 'self' data:;frame-ancestors 'self'
XATO: default-src: none tirnoqsiz — 'none' bo'lsin
XATO: script-src: 'unsafe-inline' — XSS eshigi
XATO: connect-src: * — hamma joyga ruxsat

Tirnoqsiz none — CSP'dagi eng ayyor xato: brauzer uni none degan xost nomi deb tushunadi va siyosat siz o'ylagandek ishlamaydi. Funksiya xatolarni yig'ib, bittasida to'xtamaydi — dasturchi hammasini bir martada ko'radi. frame-ancestors ni avtomatik qo'shish — "xavfsiz sukut" tamoyili (Xavfsizlik tamoyillari).

10. Loyiha qadami: API uchun qat'iy sarlavhalar

10.1 src/ilova.ts

Bitta helmet() qatori o'rniga API uchun moslangan sozlama. Telefon uchun qatorlarni bo'ldik va izohlarni qisqartirdik, kod ma'nosi kanondagidek (bo'lingan nusxa tip, lint va testlardan o'tdi — biz tekshirdik):

ts
  // helmet: API — JSON, sahifa emas. Sukutdagi CSP sahifa uchun;
  // API javobi brauzerda sahifa sifatida ochilsa (yoki XSS orqali
  // HTML qaytsa) hech narsa yuklanmasin: default-src 'none'.
  // Iframe'ga qo'yib bo'lmaydi (frame-ancestors + DENY — eski
  // brauzerlar uchun), CORP same-site — boshqa sayt <img>/<script>
  // bilan o'qiy olmaydi.
  app.use(helmet({
    contentSecurityPolicy: {
      useDefaults: false,
      directives: {
        "default-src": ["'none'"],
        "base-uri": ["'none'"],
        "form-action": ["'none'"],
        "frame-ancestors": ["'none'"],
      },
    },
    xFrameOptions: { action: "deny" },
    crossOriginResourcePolicy: { policy: "same-site" },
    // HSTS faqat HTTPS orqasida (proxy); 1 yil + subdomenlar;
    // preload — domen egasi qarori (hstspreload.org)
    strictTransportSecurity: hsts
      ? { maxAge: 31_536_000, includeSubDomains: true }
      : false,
  }));
  // Permissions-Policy — helmet qo'ymaydi: API sahifasi kamera,
  // mikrofon, joylashuvga murojaat qila olmasin
  app.use((_req, res, next) => {
    res.set(
      "Permissions-Policy",
      "camera=(), microphone=(), geolocation=(), payment=()",
    );
    next();
  });

useDefaults: false — muhim: aks holda helmet bizning to'rtta direktivamizni sukutdagi o'n bittasi bilan birlashtirardi va style-src https: kabi qiymatlar qolardi. Bu middleware'lar cors va routerlardan oldin turadi — 404 va xato javoblarida ham sarlavha bo'ladi.

10.2 Test

test/middleware.test.ts dagi helmet testi endi har qiymatni aniq tekshiradi (avval faqat "CSP bormi" edi):

ts
test("helmet sarlavhalari (API uchun qat'iy CSP) " +
  "va x-powered-by yo'q", async () => {
  const r = await xomSorov(`${asos}/v1/vazifalar`);
  const s = r.sarlavhalar;
  assert.equal(s["x-content-type-options"], "nosniff");
  assert.equal(s["content-security-policy"],
    "default-src 'none';base-uri 'none';" +
    "form-action 'none';frame-ancestors 'none'");
  assert.equal(s["x-frame-options"], "DENY");
  assert.equal(s["cross-origin-resource-policy"], "same-site");
  assert.equal(s["cross-origin-opener-policy"], "same-origin");
  assert.equal(s["referrer-policy"], "no-referrer");
  assert.equal(s["permissions-policy"],
    "camera=(), microphone=(), geolocation=(), payment=()");
  // HSTS faqat hsts: true (HTTPS proxy orqasida)
  assert.equal(s["strict-transport-security"], undefined);
  assert.equal(s["x-powered-by"], undefined);
  const xato = await xomSorov(`${asos}/yoq`); // xato javobida ham
  assert.equal(xato.sarlavhalar["x-frame-options"], "DENY");
});

Nega aniq qiymat? "Sarlavha bor" testi kimdir keyinroq 'none' ni 'self' ga almashtirsa ham o'tib ketaverardi. Aniq qiymat — siyosatning o'zini qotiradi. s qisqa nomini telefon uchun qo'shdik. HSTS yoqilgan holatni brauzer testlari to'plami (e2e/cors.e2e.ts, keyingi darsda) tekshiradi: hsts: true bilan sarlavha aynan max-age=31536000; includeSubDomains.

E2e testlarga yana bitta o'zgarish kerak — yuqoridagi kanon topilmasi. e2e/yordam.ts dagi test serveri "frontend" sahifasini API'dan oldin, tashqi Express'da beradi:

ts
// /frontend.html — "frontend" sahifasi API bilan bir origin'da
// (prod'da nginx beradi). API'ning o'z sahifalari CSP 'none' —
// ulardan fetch ham taqiqlangan, shuning uchun test sahifasi API
// middleware'laridan OLDIN, tashqi express'da
const tashqi = express();
tashqi.get("/frontend.html", (_q, r) => {
  r.type("html").send(
    "<!doctype html><meta charset=utf-8><p>frontend</p>",
  );
});
tashqi.use(ilovaYarat({
  ombor: (egasi) => new PgOmbori(pool, egasi), auth, ...qoshimcha,
}));
const server = createServer(tashqi);
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));

e2e/csrf.e2e.ts dagi "o'z sahifasidan" testi esa avval / da fetch ning to'silishini (TypeError) tekshiradi, keyin /frontend.html ga o'tadi.

10.3 Jonli tekshiruv

Serverni ishga tushirib, sarlavhasiz so'rov yuboring (biz ishga tushirdik: shu kod, Node 24.21.0, lokal PostgreSQL 17.6; Date, X-Request-Id, ETag va ulanish qatorlarini olib tashladik):

bash
curl -i "$A/v1/vazifalar?limit=1"

Chiqishni «27-qism oxiridagi sarlavhalar» bo'limidagi sukut bilan solishtiring — CSP, X-Frame-Options, CORP va yangi Permissions-Policy qatorlariga qarang:

text
HTTP/1.1 401 Unauthorized
Content-Security-Policy: default-src 'none';base-uri 'none';form-action 'none';frame-ancestors 'none'
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: DENY
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Location,X-Request-Id,Idempotency-Replayed,ETag,Link,Deprecation,Sunset
WWW-Authenticate: Cookie realm="vazifalar"
Content-Type: application/problem+json; charset=utf-8
Content-Length: 234

Nima bo'ldi? Uchta qiymat qat'iylashdi (CSP to'rtta direktiva, DENY, same-site), bittasi qo'shildi (Permissions-Policy), qolganlari sukutdagidek. 401 javobida ham hamma sarlavha bor — helmet autentifikatsiyadan oldin ishlaydi. /yoq (404) va / (public/index.html) javoblarida xuddi shu to'plam chiqdi. Access-Control-* qatorlari — keyingi darsning mavzusi.

Bu holat keyingi dars bilan birga yakunlanadi — hali commit qilmang. CORS sozlamasi va brauzer testlari qo'shilgach, ikkalasini bitta commit qilamiz.

11. Real ishda

  • Xavfsizlik skanerlari (masalan, OWASP ZAP yoki brauzerdagi "security headers" tekshiruvchilari) birinchi navbatda shu sarlavhalarni sanaydi. Hisobotda "Missing X-Frame-Options" kabi qatorlar — eng ko'p uchraydigan topilmalardan.
  • Katta loyihalarda sarlavhalarni ko'pincha reverse proxy (nginx, CDN) qo'yadi — hamma xizmat uchun bir joyda. Unda ilova va proxy bir xil sarlavhani ikki marta qo'ymasligiga e'tibor bering: ikki xil CSP bo'lsa, brauzer ikkalasini qo'llaydi va eng qattig'i yutadi.
  • CSP'ni sahifada birdaniga qattiq qilish xavfli — Content-Security-Policy-Report-Only bilan avval kuzatiladi (Brauzer xavfsizlik modeli). JSON API uchun bu bosqich kerak emas: u hech narsa yuklamaydi.
  • Intervyuda: "helmet nima qiladi?", "API'ga CSP kerakmi?", "HSTS va preload xavfi?", "clickjacking'dan qanday himoyalanasiz?".

Xulosa

  • Xavfsizlik sarlavhalari — brauzerga ko'rsatma; curl va hujumchi skriptiga ta'sir qilmaydi.
  • helmet() sukuti sahifa uchun; JSON API uchun siyosat toraytiriladi: default-src 'none' + base-uri, form-action, frame-ancestors.
  • frame-ancestors, base-uri, form-action default-src ga qaytmaydi — alohida yozish shart.
  • Clickjacking: frame-ancestors 'none' + eski brauzerlar uchun X-Frame-Options: DENY.
  • HSTS — faqat HTTPS orqasida (HSTS=true), includeSubDomains va preload — ongli qaror.
  • Permissions-Policy'ni helmet qo'ymaydi — o'zimiz qo'shdik.
  • Har qiymat test bilan qotiriladi; qat'iy CSP o'z sahifasidan fetch ni ham to'sdi — buni faqat brauzer testi ko'rsatdi.

Keyingi dars: CORS chuqur — vazifalar-api ga cookie bilan boshqa origin'dan murojaat: credentials, preflight, * tuzog'i va ruxsat ro'yxatining hiylali xatolari. O'sha darsda bu holatni commit qilamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Helmet va xavfsizlik sarlavhalari: API uchun qat'iy CSP, HSTS, clickjacking va Permissions-Policy — IlmHamroh