IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API16/51-dars14 daqiqa
Mundarija (22)

Express template engine'lari: EJS, Pug, Handlebars va server-side HTML

Qisqacha: Template engine — HTML shabloniga ma'lumot qo'yib, tayyor sahifa yasaydigan kutubxona. Express'da app.set("view engine", "ejs") va res.render("royxat", { vazifalar }) yetadi. Uchta mashhuri — EJS, Pug va Handlebars — sukut bo'yicha matnni escape qiladi (< → &lt;). Lekin har birida "xom" chiqarish bor (<%- %>, !=, {{{ }}}) — XSS shu yerdan kiradi. vazifalar-api JSON qaytaradi, shuning uchun shablon unga kirmadi — bu dars faqat tajriba.

Bu darsda

  • Template engine nima va qachon kerakligini tushuntira olasiz.
  • Express'da res.render bilan EJS sahifasini chiqarasiz, partial ulaysiz.
  • EJS, Pug va Handlebars sintaksisini taniysiz va o'lchov bilan taqqoslaysiz.
  • Auto-escape qayerda himoya qiladi va qayerda qilmaydi — bilasiz.

Oldin bilishingiz kerak: res obyekti, Mashhur middleware'lar, XSS va DOM xavfsizligi, Kontentni o'zgartirish va XSS xavfi.

1. Nega bu kerak?

Siz hozirgacha ikki xil dunyoda ishladingiz. Frontend'da (17–23-qismlar) HTML'ni brauzerda React yasadi. Backend'da (24–25) server JSON qaytardi. Lekin HTML'ni serverda yasash kerak bo'lgan joylar hali ham ko'p:

  • Email xatlari — "Parolni tiklash", "Buyurtmangiz qabul qilindi". Email mijozi JavaScript bajarmaydi — HTML tayyor kelishi shart.
  • Admin panel — 5 kishi ishlatadigan ichki sahifa uchun alohida React loyiha qurish ortiqcha.
  • Oddiy sahifalar — "Xizmat ishlamayapti", to'lov cheki, PDF uchun HTML.

Template engine bu ishni "qolip" bilan qiladi. Novvoyxonani tasavvur qiling: non qolipi bir xil, xamir har safar boshqa. Shablon — qolip, ma'lumot (vazifalar) — xamir, natija — tayyor HTML sahifa.

Satrlarni qo'lda yopishtirsa ham bo'lardi: "<li>" + matn + "</li>". Lekin bu ikki muammo beradi. Kod tez o'qilmas holga keladi. Eng yomoni — matn ichida <script> bo'lsa, u sahifada bajariladi.

2. Qo'lda shablon va escape

2.1 Muammoni ko'rish

Template engine'ning asosiy xizmati — escape (maxsus belgilarni zararsizlantirish). HTML'da <, >, &, ", ' maxsus ma'noga ega. Ularni &lt; kabi "entity"larga aylantirsangiz, brauzer ularni matn sifatida ko'rsatadi, teg sifatida emas. Bu g'oyani XSS darsida ko'rgansiz.

Quyidagi blokda kichik shablon funksiyasini o'zimiz yozamiz. U teg qilingan shablon (tagged template, tagged template darsi) — har ${} qiymatini escape qiladi. «Ishga tushir» bilan sinang:

js
const ENTITY = { "&": "&amp;", "<": "&lt;", ">": "&gt;",
  '"': "&#34;", "'": "&#39;" };
const esc = (x) => String(x).replace(/[&<>"']/g, (b) => ENTITY[b]);

// html`...` — har ${qiymat} escape qilinadi
function html(qismlar, ...qiymatlar) {
  return qismlar.reduce((acc, q, i) =>
    acc + esc(qiymatlar[i - 1]) + q);
}

const vazifalar = [
  { matn: "Non olish #uy", bajarildi: false },
  { matn: '<img src=x onerror="alert(1)">', bajarildi: true },
];
for (const v of vazifalar) {
  const sinf = v.bajarildi ? "tayyor" : "";
  console.log(html`<li class="${sinf}">${v.matn}</li>`);
}
console.log("xom:", `<li>${vazifalar[1].matn}</li>`);

Konsolda:

text
<li class="">Non olish #uy</li>
<li class="tayyor">&lt;img src=x onerror=&#34;alert(1)&#34;&gt;</li>
xom: <li><img src=x onerror="alert(1)"></li>

Ikkinchi va uchinchi qatorni solishtiring. Escape bilan hujumchining rasmi oddiy matn bo'lib qoldi. Xom satrda esa brauzer <img> tegini yaratadi, rasm yuklanmaydi va onerror ichidagi kod bajariladi.

2.2 Escape hamma narsani hal qilmaydi

Escape faqat HTML matni va qo'shtirnoqli atribut ichida himoya qiladi. Boshqa joylarda u yetmaydi:

js
const ENTITY = { "&": "&amp;", "<": "&lt;", ">": "&gt;",
  '"': "&#34;", "'": "&#39;" };
const esc = (x) => String(x).replace(/[&<>"']/g, (b) => ENTITY[b]);

const havola = "javascript:alert(document.cookie)";
console.log(`<a href="${esc(havola)}">Profil</a>`);

const xavfsizmi = (url) => /^https?:\/\//i.test(url);
const profil = "https://example.com/profil";
console.log(xavfsizmi(havola), xavfsizmi(profil));

Konsolda:

text
<a href="javascript:alert(document.cookie)">Profil</a>
false true

javascript: satrida birorta ham maxsus belgi yo'q — escape uni o'zgartirmadi. Foydalanuvchi havolani bossa, kod ishlaydi. Shuning uchun URL'ni oq ro'yxat bilan tekshirasiz: faqat http:// va https://. <script> ichiga, style atributiga yoki onclick ga foydalanuvchi ma'lumotini qo'yish esa umuman taqiqlanadi. Har kontekst o'z qoidasiga ega.

Tekshirib ko'ring: <div title=${esc(ism)}> — qo'shtirnoqsiz atribut. ism = x onmouseover=alert(1) bo'lsa nima bo'ladi?

Javob

Escape bu satrda hech narsani o'zgartirmaydi: unda <, >, ", ', & yo'q. Natija: <div title=x onmouseover=alert(1)> — brauzer bo'shliqni atributlar chegarasi deb biladi va onmouseover alohida atribut bo'ladi. Qoida: atribut qiymati har doim qo'shtirnoq ichida. Template engine'lar ham shunday shablon kutadi.

3. Express va EJS

3.1 res.render

Express shablonlarni res.render(nom, ma'lumot) bilan chiqaradi (res obyekti). Ikki sozlama kerak: qaysi engine va shablonlar qayerda.

js
import express from "express"; // + npm i ejs

const app = express();
app.set("view engine", "ejs"); // .ejs fayllar
app.set("views", "./views"); // sukut: ./views

app.get("/", (req, res) => {
  res.render("royxat", { vazifalar }); // views/royxat.ejs
});

app.set("view engine", "ejs") yozilganda Express ejs paketini o'zi import qiladi — kodda import ejs shart emas. Lekin paket o'rnatilgan bo'lishi kerak: npm i ejs.

3.2 Shablon va partial'lar

views/royxat.ejs:

html
<%- include("qismlar/bosh", { sarlavha: "Vazifalar" }) %>
<h1>Vazifalar (<%= vazifalar.length %>)</h1>
<ul>
<% for (const v of vazifalar) { -%>
  <li class="<%= v.bajarildi ? "tayyor" : "" %>"><%= v.matn %></li>
<% } -%>
</ul>
<%- include("qismlar/oxir") %>

EJS'da uch xil teg bor:

Teg Nima qiladi
<%= x %> x ni escape qilib chiqaradi
<%- x %> x ni xom chiqaradi (escape yo'q)
<% kod %> JS kodi: for, if — chiqarmaydi
-%> Tegdan keyingi qator ko'chishini olib tashlaydi

Partial — bir nechta sahifada takrorlanadigan bo'lak (sarlavha, menyu, pastki qism). EJS'da include() bilan ulanadi. include tayyor HTML qaytaradi, shuning uchun u <%- (xom) bilan chiqariladi — bu xavfsiz, chunki partial ichidagi qiymatlar o'zi <%= bilan escape qilingan. views/qismlar/bosh.ejs:

html
<!doctype html>
<html lang="uz">
<head><meta charset="utf-8"><title><%= sarlavha %></title></head>
<body>

Layout — har sahifani o'rab turadigan umumiy qolip. EJS'ning o'zida layout yo'q, shuning uchun bosh va oxir partial'lari bilan quriladi. Pug'da extends/block, Handlebars'da express-handlebars paketining layouts papkasi bor.

3.3 Natija

Biz ishga tushirdik (express 5.3.0, ejs 7.0.1). Ma'lumot — ikki vazifa, biri hujumchinikidek. Ikkinchi <li> ichiga qarang:

text
200 text/html; charset=utf-8
<!doctype html>
<html lang="uz">
<head><meta charset="utf-8"><title>Vazifalar</title></head>
<body>

<h1>Vazifalar (2)</h1>
<ul>
  <li class="">Non olish #uy</li>
  <li class="tayyor">&lt;img src=x onerror=&#34;alert(1)&#34;&gt;</li>
</ul>
</body></html>

Nima bo'ldi? Bosh va oxir partial'lari sahifani o'radi, res.render Content-Type: text/html ni o'zi qo'ydi. <%= v.matn %> — escape qilingan, yuqorida qo'lda yozgan funksiyamiz bilan bir xil natija. Mavjud bo'lmagan shablon so'ralsa — xato xato middleware'iga boradi:

text
500 Failed to lookup view "yoq-shablon" in views directory "./views"

"./views papkasida yoq-shablon ko'rinishi topilmadi". Bu xabarni foydalanuvchiga ko'rsatmang — u server papka tuzilmasini oshkor qiladi.

flowchart LR
  A["GET /"] --> B["handler:<br/>res.render('royxat', data)"]
  B --> C["views/royxat.ejs<br/>+ qismlar/*.ejs"]
  C --> D["EJS: kompilyatsiya<br/>(keshlanadi)"]
  D --> E["HTML satri"]
  E --> F["200 text/html"]

Diagrammadagi "kompilyatsiya" qadamiga qarang. Shablon birinchi marta JS funksiyasiga aylantiriladi, keyin faqat funksiya chaqiriladi. Express NODE_ENV=production da shu funksiyani keshlaydi (view cache). Development'da esa har so'rovda faylni qayta o'qiydi — shablonni tahrirlasangiz, serverni qayta ishga tushirmasdan ko'rasiz.

Tekshirib ko'ring: Admin "e'lon matnini" HTML formatlash bilan saqlaydi va siz uni <%- elon %> bilan chiqarasiz. Qanday xavf bor va nima qilish kerak?

Javob

Xom chiqarish (stored XSS): admin akkaunti o'g'irlansa yoki admin bilmasdan <script> joylasa, kod har foydalanuvchida bajariladi. Yechim: HTML'ni saqlashdan oldin yoki chiqarishdan oldin sanitizatsiya qilish — faqat ruxsat etilgan teglarni qoldiradigan kutubxona bilan (masalan, DOMPurify). Yoki HTML o'rniga Markdown saqlab, xavfsiz renderer ishlatish. Sanitizatsiya React XSS himoyasi darsida chuqur.

4. Uch engine yonma-yon

Kanon t16 bir xil ro'yxatni uchala engine'da yasagan. Sintaksis:

EJS Pug Handlebars
Escape bilan <%= x %> = x yoki #{x} {{x}}
Xom <%- x %> != x {{{x}}}
Sikl <% for … %> each v in vazifalar {{#each vazifalar}}
Layout partial bilan extends / block express-handlebars

Pug — chekinish (indent) asosidagi alohida til, HTML teglari yozilmaydi (li.tayyor= v.matn). Handlebars — "mantiqsiz" shablon: ichida JS kodi yozilmaydi, faqat yordamchilar (helpers). EJS — oddiy HTML ichida oddiy JS. HTML va JS biladigan dasturchi uchun eng qisqa yo'l.

Uchalasining escape natijasi (kanon chiqishi, qisqartirilgan). Qo'shtirnoq qanday kodlanganiga va oxirgi qatorga qarang:

text
ejs:
<li class="tayyor">&lt;script&gt;alert(&#34;xss&#34;)&lt;/script&gt;</li>
pug va handlebars:
<li class="tayyor">&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</li>
xom teg (uchalasida):
<p>xom: <script>alert("xss")</script></p>

Qo'shtirnoq turlicha kodlangan (&#34; va &quot;) — brauzer uchun ikkalasi bir xil. Oxirgi qatorga e'tibor bering: xom teg uchala engine'da ham <script> ni o'zgartirmay chiqardi.

Narx (kanon o'lchovi, 2026-10-24):

O'rnatish hajmi: bitta engine
  • ejs 7.0.11 paket0,2 MB
  • handlebars 4.7.106 paket5,2 MB
  • pug 3.0.451 paket6,9 MB

Manba: IlmHamroh kanon o'lchovi t16, 2026-10-24: har paket alohida bo'sh papkaga npm i (prod), Node 24.21.0

Grafikda farq 35 baravargacha: EJS bitta paket, Pug esa 51 paket o'rnatadi. Tezlikda esa Pug oldinda: kompilyatsiyadan keyin sekundiga 1 075 656 render, EJS 223 930, Handlebars 235 333. Lekin bu kichik ro'yxatda o'lchangan. Real sahifada vaqtni ma'lumotlar bazasi so'rovi oladi, shablon emas — 0.004 ms va 0.001 ms farqni foydalanuvchi sezmaydi.

5. Zamonaviy alternativalar

Template engine'lar 2010-yillar uslubi. Bugun server HTML'i uchun yana ikki yo'l bor:

  • React serverda. Siz bilgan JSX bilan: renderToString(<Royxat vazifalar={v} />). React escape'ni o'zi qiladi, xom chiqarish faqat dangerouslySetInnerHTML bilan — nomidan xavfli ekani ko'rinib turadi. Next.js aynan shunday ishlaydi (Next.js qismi).
  • Email shablonlari — React Email yoki MJML kabi vositalar: oddiy komponentdan email mijozlari tushunadigan eski uslubdagi HTML (jadvallar, inline CSS) yasaydi.

Qachon nima:

Vaziyat Tanlov
To'liq frontend, SEO Next.js
Kichik admin sahifa, Express ichida EJS
Email xatlari React Email / MJML yoki EJS
Faqat JSON API Shablon kerak emas

6. API qadami: shablon kerakmi? (holat 17, 3-qism)

vazifalar-api ga shablon kirmadi. Sabab oddiy: API faqat JSON qaytaradi, HTML'ni frontend (vazifalar React ilovasi) yasaydi. public/index.html — o'zgarmas statik sahifa, uni express.static beradi (Mashhur middleware'lar). Shablon qo'shish — yangi bog'liqlik va yangi XSS yuzasi, foydasi esa yo'q.

Kodda o'zgarish yo'q — hali commit qilmang, holat 17 ning commiti keyingi darsda. Kelajakda email kerak bo'ladi ("parolni tiklash" — auth qismida). O'shanda shablon tanlovi shu darsdagi jadval bilan qilinadi.

7. Hujumchi nigohi

Hujum Qayerda Himoya
Stored XSS <%- matn %>, {{{matn}}}, != matn Faqat escape qiluvchi teg; HTML kerak bo'lsa — sanitizatsiya
javascript: havola href="<%= url %>" URL oq ro'yxati: faqat http(s)://
Qo'shtirnoqsiz atribut title=<%= x %> Atribut doim "…" ichida
Shablon nomi foydalanuvchidan res.render(req.query.sahifa) Nomlar ro'yxati; foydalanuvchi matnini nom qilmang
Xato matni Failed to lookup view … in views directory … Prod'da umumiy xato sahifasi

To'rtinchi qator alohida xavfli. Foydalanuvchi shablon nomini tanlasa, u boshqa faylni ochishga urinadi. Bundan ham yomoni — shablonni foydalanuvchi matnidan kompilyatsiya qilish (ejs.render(req.body.shablon)). Bu SSTI (Server-Side Template Injection): EJS ichida JS ishlaydi, demak hujumchi serverda kod bajaradi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Cannot find module 'ejs' view engine berilgan, paket yo'q npm i ejs
Failed to lookup view Nom yoki views papkasi noto'g'ri app.set("views", …), nomni tekshiring
Sahifada &lt;b&gt; ko'rinadi Partial <%= bilan ulangan include — <%- bilan
Shablon o'zgarishi ko'rinmaydi Prod'da view cache Qayta ishga tushirish
alert ishladi Xom teg <%=, sanitizatsiya

9. Mashqlar

Yechimlar kurs/mashqlar/25/16-shablonlar/ da.

1-mashq (oson): to'ldiring

  • EJS'da escape qilib chiqaradigan teg — <%
  • Handlebars'da xom chiqarish —
  • Express'da shablonni chiqaradigan metod — res.
  • < belgisining HTML entity'si —
Yechim
  • = — <%= x %>; <%- x %> esa xom.
  • {{{x}}} — uch qavs; ikki qavs {{x}} escape qiladi.
  • render — res.render("royxat", { vazifalar }).
  • < — "less than".

2-mashq (o'rta): chek shabloni

html teg funksiyasidan foydalanib, chekYasa(buyurtma) funksiyasini yozing. U <table> qaytarsin: har taom — qator (nom, soni, summa), oxirida "Jami". Taom nomi escape qilinsin. Sinov: «Bahor» oshxonasi buyurtmasi — osh 2 × 35 000, ko'k choy 1 × 5 000 va nomi <b>Manti</b> bo'lgan taom 1 × 30 000. Yordam: reduce — reduce darsi, html — «Qo'lda shablon va escape» bo'limida. Toza JS — «Ishga tushir» bilan ishlaydi.

Yechim
js
const ENTITY = { "&": "&amp;", "<": "&lt;", ">": "&gt;",
  '"': "&#34;", "'": "&#39;" };
const esc = (x) => String(x).replace(/[&<>"']/g, (b) => ENTITY[b]);
function html(qismlar, ...qiymatlar) {
  return qismlar.reduce((acc, q, i) =>
    acc + esc(qiymatlar[i - 1]) + q);
}

function chekYasa(taomlar) {
  const qatorlar = taomlar.map((t) =>
    html`<tr><td>${t.nom}</td><td>${t.soni}</td>` +
    html`<td>${t.soni * t.narx}</td></tr>`);
  const jami = taomlar.reduce((s, t) => s + t.soni * t.narx, 0);
  // qatorlar allaqachon xavfsiz HTML — qayta escape qilinmaydi
  return ["<table>", ...qatorlar,
    html`<tr><th colspan="2">Jami</th><td>${jami}</td></tr>`,
    "</table>"].join("\n");
}

console.log(chekYasa([
  { nom: "Osh", soni: 2, narx: 35000 },
  { nom: "Ko'k choy", soni: 1, narx: 5000 },
  { nom: "<b>Manti</b>", soni: 1, narx: 30000 },
]));

Konsolda:

text
<table>
<tr><td>Osh</td><td>2</td><td>70000</td></tr>
<tr><td>Ko&#39;k choy</td><td>1</td><td>5000</td></tr>
<tr><td>&lt;b&gt;Manti&lt;/b&gt;</td><td>1</td><td>30000</td></tr>
<tr><th colspan="2">Jami</th><td>105000</td></tr>
</table>

Diqqat: qatorlar — tayyor HTML. Uni yana html ichiga ${} bilan qo'ysak, ikki marta escape bo'lib, sahifada &lt;tr&gt; ko'rinardi. EJS'da include ni <%- bilan ulash ham shu sababdan.

3-mashq (qiyin): xavfsiz havola

havolaYasa(matn, url) funksiyasini yozing: url faqat http: yoki https: protokolida bo'lsa <a href="…">matn</a>, aks holda faqat escape qilingan matn qaytarsin. Protokolni regex bilan emas, URL klassi bilan aniqlang (URL darsi) — new URL xato bersa, havola yo'q. To'rt holatni sinang: oddiy https, javascript:, katta harfli JAVASCRIPT:, nisbiy /profil.

Yechim
js
const ENTITY = { "&": "&amp;", "<": "&lt;", ">": "&gt;",
  '"': "&#34;", "'": "&#39;" };
const esc = (x) => String(x).replace(/[&<>"']/g, (b) => ENTITY[b]);

function havolaYasa(matn, url) {
  let protokol;
  try {
    protokol = new URL(url).protocol;
  } catch {
    return esc(matn); // nisbiy yoki buzuq URL
  }
  if (protokol !== "http:" && protokol !== "https:") return esc(matn);
  return `<a href="${esc(url)}">${esc(matn)}</a>`;
}

console.log(havolaYasa("Sayt", "https://example.com/?a=1&b=2"));
console.log(havolaYasa("Profil", "javascript:alert(1)"));
console.log(havolaYasa("Profil", "JAVASCRIPT:alert(1)"));
console.log(havolaYasa("<Profil>", "/profil"));

Konsolda:

text
<a href="https://example.com/?a=1&amp;b=2">Sayt</a>
Profil
Profil
&lt;Profil&gt;

4-mashq: API qadami

vazifalar-api ga shablon qo'shmang. Uning o'rniga loyiha README ga bitta qator yozing: "HTML — frontend'da; API faqat JSON". Kelajakdagi email uchun 2-mashqdagi kabi toza funksiya yetadimi yoki EJS kerakmi — o'z fikringizni bir gap bilan qo'shing. Commit qilmang.

Yechim

Namuna: "Shablon yo'q: API faqat JSON qaytaradi, HTML'ni vazifalar frontend'i yasaydi. Email (28-qism) uchun 1–2 ta oddiy xat bo'lsa — escape qiluvchi toza funksiya, ko'p va murakkab bo'lsa — EJS (1 paket, 0.2 MB)." Muhimi — qarorni o'lchov va ehtiyojga bog'lash.

10. Real ishda

  • Express + EJS — eski va kichik loyihalarda, admin panellarda tez-tez uchraydi. Ko'p o'zbek kompaniyalarining ichki tizimlari shunday yozilgan.
  • Email — har backend'da bor: ro'yxatdan o'tish, parol tiklash, chek. Bu yerda escape xatosi foydalanuvchining pochtasiga XSS emas, lekin phishing havola olib kirishi mumkin.
  • Xavfsizlik auditi — <%-, {{{, != va dangerouslySetInnerHTML ni grep qilish — birinchi qadam.
  • Intervyu: "Stored va reflected XSS farqi?", "Escape va sanitizatsiya farqi?"

Xulosa

  • Template engine — qolip + ma'lumot = HTML; Express'da view engine va res.render.
  • EJS <%= %>, Pug =, Handlebars {{ }} — escape qiladi; <%- %>, !=, {{{ }}} — xom, XSS manbai.
  • Escape faqat matn va qo'shtirnoqli atributda yetadi; URL — oq ro'yxat, <script>/on* — taqiq.
  • EJS 1 paket va 0.2 MB, Pug 51 paket va 6.9 MB; render tezligidagi farq real sahifada sezilmaydi.
  • vazifalar-api — JSON API, shablon kirmadi; HTML'ni frontend yasaydi.

Keyingi dars: Express + TypeScript — Request generiklari, req.sorovId uchun declaration merging va holat 17 ning commiti.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Express template engine'lari: EJS, Pug, Handlebars va server-side HTML — IlmHamroh