Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. MIME turi: brauzerga "bu nima" deyish
- 2.1 Content-Type qayerdan olinadi
- 2.2 nosniff haqida bir gap
- 3. Faylni stream bilan berish
- 3.1 Nega readFile emas
- 3.2 Content-Length — oldindan aytish
- 4. ETag va 304: o'zgarmagan faylni qayta yubormaslik
- 4.1 Shartli so'rov qanday ishlaydi
- 4.2 ETag nimadan yasaladi
- 4.3 Hammasi bitta misolda
- 5. Range va 206: faylning bir bo'lagi
- 5.1 Video va yuklashni davom ettirish
- 5.2 Tahlil — toza funksiya
- 5.3 Haqiqiy server bilan
- 6. Path traversal: eng xavfli xato
- 6.1 Hujum qanday ko'rinadi
- 6.2 O'lchov: fayl o'qildi
- 6.3 Himoya: resolve va startsWith
- 7. Hujumchi nigohi
- 8. API qadami: static fayllar va holat 05 (commit)
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): Content-Range yasash
- 3-mashq (qiyin): xavfsiz yo'l
- 4-mashq: API qadami
- 11. Real ishda
- Xulosa
- Manbalar
Static fayl server Node.js'da: MIME, ETag va 304, Range va 206, path traversal himoyasi
Qisqacha: Static fayl server — diskdagi faylni (HTML, CSS, rasm, video) o'zgartirmasdan beradigan qism. To'g'ri ishlashi uchun to'rt narsa kerak: to'g'ri MIME turi (
Content-Type), faylni stream bilan berish, kesh uchun ETag va 304, video uchun Range va 206. Eng xavflisi — path traversal:/..%2fmaxfiy.envkabi so'rovpublic/dan tashqaridagi faylni o'qib yuborishi mumkin. Biz o'lchadik: faqatjoin()bilan yozilgan serverDB_PAROL=...qatorini berib yubordi,resolve()+startsWith()tekshiruvi esa to'rt hujumni ham to'xtatdi.
Bu darsda
- Fayl kengaytmasidan MIME turini tanlaysiz va nega
application/octet-streamzaxira ekanini tushuntirasiz. - Faylni
pipelinebilan stream qilib berasiz. ETag,Last-Modified,If-None-Matchva304 Not Modifiedqanday ishlashini ko'rasiz.Rangesarlavhasini tahlil qilib,206va416javob berasiz.- Path traversal hujumini o'z ko'zingiz bilan ko'rasiz va undan himoya yozasiz;
vazifalar-apining birinchi holatini commit qilasiz.
Oldin bilishingiz kerak: Request body o'qish va hajm cheklovi, Xavfsiz fayl operatsiyalari, pipeline, finished va stream xatolari, HTTP sarlavhalari, MIME turlari, cookie va kesh.
1. Nega bu kerak?
vazifalar-api hozir JSON qaytaradi: GET /vazifalar, POST /vazifalar va boshqalar. Lekin API yonida deyarli doim "oddiy" fayllar ham bo'ladi: bosh sahifa (index.html), API hujjati, favicon, ba'zan rasm yoki video. Ular bazadan emas, diskdan olinadi va o'zgarmasdan beriladi. Shuning uchun ularni static fayllar (static files) deymiz — "qo'zg'almas", har so'rovda bir xil.
Birinchi qarashda ish oddiy: faylni o'qing va javobga yozing. Lekin to'rtta savol darhol chiqadi:
- Brauzer
menyu.cssni CSS deb qayerdan biladi? (MIME turi) - 200 MB lik videoni xotiraga to'liq o'qisak nima bo'ladi? (stream)
- Fayl o'zgarmagan bo'lsa, uni har safar qayta yuborish shartmi? (ETag va 304)
- Foydalanuvchi videoning 40-daqiqasiga sakrasa-chi? (Range va 206)
Beshinchi savol esa eng muhimi: so'rov public/ papkadan tashqaridagi faylni so'rasa-chi? Masalan, parollar yozilgan .env faylini.
O'xshatish: kutubxonachi. U kitobni javondan olib beradi (fayl), muqovasiga janrini yozadi (MIME), "sizda shu nashr bor-ku" desangiz qayta bermaydi (304), "faqat 5-bobni bering" desangiz o'shani nusxalaydi (206). Lekin "javonlar orqasidagi xonadan direktorning seyfini olib bering" desangiz — rad etadi (traversal himoyasi).
flowchart TD
A["GET /index.html"] --> B{"public/ ichidami?"}
B -- yo'q --> X["404"]
B -- ha --> C{"fayl bormi?"}
C -- yo'q --> X
C -- ha --> D{"If-None-Match = ETag?"}
D -- ha --> E["304, tanasiz"]
D -- yo'q --> F{"Range bormi?"}
F -- yo'q --> G["200 + butun fayl"]
F -- yaroqli --> H["206 + bo'lak"]
F -- yaroqsiz --> I["416"]Diagrammani yuqoridan pastga o'qing: har romb — bitta tekshiruv. Darsning qolgan qismi aynan shu tartibda boradi, faqat MIME turidan boshlaymiz — u har javobda kerak.
2. MIME turi: brauzerga "bu nima" deyish
2.1 Content-Type qayerdan olinadi
MIME turlari darsida ko'rgansiz: MIME turi (MIME type) — text/html, image/png kabi yorliq, u Content-Type sarlavhasida boradi. Brauzer fayl kengaytmasiga emas, aynan shu yorliqqa qaraydi. Server style.css ni text/plain deb yuborsa, brauzer uni stil sifatida qo'llamaydi.
Node'da MIME jadvali yo'q — uni o'zimiz yozamiz. vazifalar-api dagi jadval kichik: faqat bizga kerak bo'lgan sakkizta kengaytma. Quyidagi blok shu g'oyani ko'rsatadi (brauzerda ham ishlaydi):
const MIME = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
};
function mimeOl(faylNomi) {
const nuqta = faylNomi.lastIndexOf(".");
const ext = nuqta === -1 ? "" : faylNomi.slice(nuqta);
return MIME[ext.toLowerCase()] ?? "application/octet-stream";
}
const nomlar = ["index.html", "LOGO.PNG", "arxiv.tar.gz", "Makefile"];
for (const nom of nomlar) {
console.log(nom, "→", mimeOl(nom));
}Konsolda:
index.html → text/html; charset=utf-8
LOGO.PNG → image/png
arxiv.tar.gz → application/octet-stream
Makefile → application/octet-streamUch narsaga e'tibor bering:
toLowerCase()—LOGO.PNGham rasm. Windows kengaytma harfini farqlamaydi, foydalanuvchilar esa ikkalasini ham yozadi.charset=utf-8— matnli turlarda kodlashni aytamiz. Busiz o'zbekchao'vag'dan keyingi belgilar (masalan—yoki kirill harflari) buzilib chiqishi mumkin.application/octet-stream— "noma'lum baytlar" degani. Brauzer bunday faylni ochmaydi, yuklab olishni taklif qiladi. Noma'lum kengaytmaga bu xavfsiz tanlov:text/htmldeb taxmin qilsak, yuklangan fayl ichidagi skript brauzerda ishga tushib ketishi mumkin.
Maslahat: Katta loyihalarda jadvalni qo'lda yozmaysiz —
mime-typespaketi yuzlab kengaytmani biladi. Express ham shuni ishlatadi (Express'ga kirish darsida ko'rasiz). Bizning sakkiz qatorli jadval esa bog'liqliksiz va to'liq nazoratda.
2.2 nosniff haqida bir gap
Ba'zi brauzerlar Content-Type noto'g'ri bo'lsa, faylning boshidagi baytlarga qarab turini "taxmin" qiladi. Bu MIME sniffing (turni hidlash) deyiladi va xavfli: rasm deb yuklangan fayl HTML sifatida ochilishi mumkin. X-Content-Type-Options: nosniff sarlavhasi taxminni taqiqlaydi. Uni mashhur middleware'lar darsida helmet kutubxonasi o'zi qo'yadi.
Tekshirib ko'ring: Server
.webprasmlarni beryapti, lekin jadvalda.webpyo'q. Brauzerda nima bo'ladi?
Javob
mimeOl zaxira turni qaytaradi — application/octet-stream. <img> tegidagi rasm ko'pincha baribir chiqadi (brauzer rasm baytlarini taniydi), lekin faylni manzil satri orqali ochsangiz, brauzer uni yuklab olishni taklif qiladi. To'g'risi — jadvalga ".webp": "image/webp" qo'shish.
3. Faylni stream bilan berish
3.1 Nega readFile emas
readFile butun faylni xotiraga o'qiydi. 2 KB lik index.html uchun bu muammo emas. 300 MB lik video uchun esa har so'rov 300 MB xotira oladi; o'nta foydalanuvchi — 3 GB. Stream darsida buning yechimini ko'rgansiz: fayl bo'laklab o'qiladi va har bo'lak darhol tarmoqqa yoziladi.
res — o'zi yoziladigan stream (createServer darsi). Demak fayl stream'ini to'g'ridan unga ulaymiz:
await pipeline(createReadStream(fayl), res);Nega .pipe() emas, pipeline? pipeline darsida ko'rganingizdek, .pipe() xatoni uzatmaydi: fayl o'qishda xato bo'lsa, javob ochiq qolib ketishi mumkin. pipeline esa ikkala tomonni ham yopadi va xatoni await orqali qaytaradi. Mijoz yuklashni yarmida to'xtatsa ham (res yopilsa), pipeline fayl stream'ini o'zi yopadi — ochiq fayl deskriptori qolmaydi.
3.2 Content-Length — oldindan aytish
Stream bilan yozganda Node fayl hajmini bilmaydi. Content-Length bermasak, u chunked kodlashga o'tadi (HTTP server tomonda darsi). Bu ishlaydi, lekin brauzer yuklash foizini ko'rsata olmaydi. Hajmni stat() bilan oldindan bilamiz va sarlavhaga yozamiz. Pastdagi to'liq misolda shuni ko'rasiz.
4. ETag va 304: o'zgarmagan faylni qayta yubormaslik
4.1 Shartli so'rov qanday ishlaydi
Brauzer index.html ni birinchi marta oladi. Javobda server ETag (entity tag — "versiya yorlig'i") qo'yadi: fayl o'zgarsa o'zgaradigan qisqa satr. Brauzer faylni ham, yorliqni ham keshda saqlaydi.
Keyingi safar brauzer so'raydi: "menda W/"109-1a16bdb0c32" versiya bor, o'zgarganmi?" Buni If-None-Match sarlavhasi bilan aytadi. Bunday so'rov shartli so'rov (conditional request) deyiladi. Versiya bir xil bo'lsa, server 304 Not Modified qaytaradi — tanasiz. Brauzer fayl o'rniga keshdagisini ishlatadi.
sequenceDiagram
participant B as Brauzer
participant S as Server
B->>S: GET /index.html
S-->>B: 200, ETag: W/"109-1a16bdb0c32", 265 bayt
B->>S: GET /index.html, If-None-Match: W/"109-1a16bdb0c32"
S-->>B: 304 Not Modified, 0 baytIkkinchi javobda tana yo'q. Fayl katta bo'lsa — tejash ham katta.
4.2 ETag nimadan yasaladi
Eng aniq yo'l — fayl mazmunining hash'i (crypto darsi). Lekin buning uchun har so'rovda butun faylni o'qish kerak. Static serverlar odatda arzonroq yo'l tanlaydi: hajm + o'zgartirilgan vaqt (mtime). vazifalar-api shunday qiladi:
const etag = `W/"${st.size.toString(16)}-${
Math.floor(st.mtimeMs).toString(16)}"`;109 — 265 baytning o'n oltilik ko'rinishi, ikkinchi qism — millisekundlardagi vaqt. Boshidagi W/ — kuchsiz (weak) ETag: "mazmuni deyarli bir xil" degani, baytma-bayt kafolat emas. Kuchli va kuchsiz ETag farqi HTTP kesh darsida muhim bo'ladi; static fayl uchun kuchsizi yetadi. Qiziq fakt: Express'ning static middleware'i ham aynan shu formatni ishlatadi — keyingi darslarda W/"109-1a16bdb0c33" ni ko'rasiz.
Last-Modified — xuddi shu g'oyaning eski varianti: sana bilan (Sat, 24 Oct 2026 03:01:20 GMT). Brauzer uni If-Modified-Since bilan qaytaradi. Aniqligi — bir soniya, shuning uchun bir soniyada ikki marta o'zgargan faylni sezmaydi. Ikkalasini birga yuborish odatiy hol.
Cache-Control: public, max-age=0, must-revalidate — "saqlab qo'yishing mumkin, lekin har safar ishlatishdan oldin men bilan tekshir". Shunda fayl yangilansa, brauzer eski versiyani ko'rsatib turmaydi.
4.3 Hammasi bitta misolda
Quyidagi blok kichik server ochadi: bitta fayl, ETag, 304 va oddiy Range. Keyin o'ziga uchta so'rov yuboradi. Blok Node uchun — o'z terminalingizda ishga tushiring:
import { createServer } from "node:http";
import { createReadStream } from "node:fs";
import { mkdir, writeFile, stat } from "node:fs/promises";
import { pipeline } from "node:stream/promises";
await mkdir("public", { recursive: true });
await writeFile("public/menyu.txt", "osh 35000\nmanti 30000\n");
const server = createServer(async (req, res) => {
const fayl = "public/menyu.txt"; // soddalik uchun — bitta fayl
const st = await stat(fayl);
const etag = `W/"${st.size}-${Math.floor(st.mtimeMs)}"`;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.setHeader("ETag", etag);
res.setHeader("Accept-Ranges", "bytes");
if (req.headers["if-none-match"] === etag) {
return res.writeHead(304).end();
}
const m = /^bytes=(\d+)-(\d+)$/.exec(req.headers.range ?? "");
if (m) {
const [bosh, oxir] = [Number(m[1]), Number(m[2])];
res.writeHead(206, {
"Content-Range": `bytes ${bosh}-${oxir}/${st.size}`,
});
const oqim = createReadStream(fayl, { start: bosh, end: oxir });
return pipeline(oqim, res);
}
res.writeHead(200, { "Content-Length": st.size });
await pipeline(createReadStream(fayl), res);
});
await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
const url = `http://127.0.0.1:${server.address().port}/menyu.txt`;
const r1 = await fetch(url);
const etag = r1.headers.get("etag");
console.log(r1.status, JSON.stringify(await r1.text()));
const r2 = await fetch(url, { headers: { "If-None-Match": etag } });
console.log(r2.status, JSON.stringify(await r2.text()));
const r3 = await fetch(url, { headers: { Range: "bytes=0-2" } });
console.log(r3.status, r3.headers.get("content-range"));
console.log(JSON.stringify(await r3.text()));
server.closeAllConnections();
server.close();Konsolda:
200 "osh 35000\nmanti 30000\n"
304 ""
206 bytes 0-2/22
"osh"Birinchi so'rov — butun fayl (22 bayt). Ikkinchisi ETag'ni qaytardi va bo'sh 304 oldi. Uchinchisi faqat 0–2 baytlarni so'radi va osh so'zini oldi.
Diqqat:
createReadStreamningendopsiyasi ichiga oladi:start: 0, end: 2— uchta bayt (0, 1, 2). HTTP'dagibytes=0-2ham xuddi shunday. Shu sababliContent-Length=oxir - bosh + 1. Bitta birlikni unutish — Range kodidagi eng ko'p uchraydigan xato.
Tekshirib ko'ring: Server 304 javobiga ham
ETagsarlavhasini qo'ydi. Nega bu foydali?
Javob
304 — "keshdagi nusxang hali yaroqli" degani. Javobdagi sarlavhalar (ETag, Cache-Control) keshdagi yozuvni yangilaydi. Masalan, server Cache-Control ni o'zgartirgan bo'lsa, brauzer buni tanani qayta yuklamasdan bilib oladi.
5. Range va 206: faylning bir bo'lagi
5.1 Video va yuklashni davom ettirish
video tegi bilan sahifadagi videoni o'rtasiga sursangiz, brauzer butun faylni yuklamaydi. U so'raydi: Range: bytes=31457280-, ya'ni "30 MB dan boshlab oxirigacha". Server 206 Partial Content va Content-Range: bytes 31457280-…/… bilan javob beradi. Uzilgan yuklashni davom ettirish (download manager'lar, curl -C -) ham shu mexanizmda ishlaydi.
Server Range'ni qo'llashini Accept-Ranges: bytes sarlavhasi bilan e'lon qiladi. Bu sarlavha bo'lmasa, ba'zi pleyerlar videoni sura olmaydi.
Range uch ko'rinishda keladi:
| Sarlavha | Ma'nosi | 265 baytlik faylda |
|---|---|---|
bytes=0-14 |
0 dan 14 gacha | 0–14, 15 bayt |
bytes=100- |
100 dan oxirigacha | 100–264 |
bytes=-8 |
oxirgi 8 bayt | 257–264 |
Agar so'ralgan joy fayldan tashqarida bo'lsa (bytes=99999-), server 416 Range Not Satisfiable qaytaradi va Content-Range: bytes */265 bilan haqiqiy hajmni aytadi. Bu qoidalar — RFC 9110, "Range Requests" bo'limi.
5.2 Tahlil — toza funksiya
Range'ni tahlil qilish — tarmoqsiz, toza mantiq. Uni alohida funksiyaga ajratsak, brauzerda ham sinash mumkin. Bu vazifalar-api dagi mantiqning qisqartirilgan ko'rinishi:
// natija: undefined — Range yo'q; null — 416; aks holda oraliq
function rangeOl(sarlavha, hajm) {
if (sarlavha === undefined) return undefined;
const m = /^bytes=(\d*)-(\d*)$/.exec(sarlavha);
if (!m || (m[1] === "" && m[2] === "")) return null;
let bosh;
let oxir = hajm - 1;
if (m[1] === "") {
bosh = Math.max(0, hajm - Number(m[2])); // bytes=-8
} else {
bosh = Number(m[1]);
if (m[2] !== "") oxir = Math.min(Number(m[2]), hajm - 1);
}
if (bosh > oxir || bosh >= hajm) return null;
return { bosh, oxir };
}
const sarlavhalar = [
undefined, "bytes=0-14", "bytes=100-", "bytes=-8",
"bytes=99999-", "bytes=0-1,5-9", "elementlar=0-5",
];
for (const s of sarlavhalar) {
console.log(String(s).padEnd(16), JSON.stringify(rangeOl(s, 265)));
}Konsolda:
undefined undefined
bytes=0-14 {"bosh":0,"oxir":14}
bytes=100- {"bosh":100,"oxir":264}
bytes=-8 {"bosh":257,"oxir":264}
bytes=99999- null
bytes=0-1,5-9 null
elementlar=0-5 nullbytes=0-1,5-9 — bir nechta oraliq. Standart bunga ruxsat beradi, lekin javob multipart/byteranges formatida murakkab bo'ladi. Ko'p serverlar, jumladan bizniki, faqat bitta oraliqni qo'llaydi. Bu yerda 416 qaytaramiz. Standart bunday holda oddiy 200 bilan butun faylni berishga ham ruxsat beradi. Bizning tanlov qat'iyroq, lekin oddiy.
5.3 Haqiqiy server bilan
vazifalar-api holat 05 dagi index.html 265 bayt. Biz ishga tushirdik (Node 24.21.0, Windows 11, curl 8.19.0). A — server manzili, masalan http://127.0.0.1:3000:
curl -i $A/index.html -H "Range: bytes=-8"HTTP/1.1 206 Partial Content
Content-Type: text/html; charset=utf-8
ETag: W/"109-1a16c9d2430"
Last-Modified: Sat, 24 Oct 2026 06:33:20 GMT
Cache-Control: public, max-age=0, must-revalidate
Accept-Ranges: bytes
Content-Range: bytes 257-264/265
Content-Length: 8
Connection: keep-alive
Keep-Alive: timeout=5
</html>Oxirgi 8 bayt — </html> va qator oxiri. Endi fayldan tashqaridagi joy:
curl -i $A/index.html -H "Range: bytes=99999-"HTTP/1.1 416 Range Not Satisfiable
Content-Type: text/html; charset=utf-8
ETag: W/"109-1a16c9d2430"
Last-Modified: Sat, 24 Oct 2026 06:33:20 GMT
Cache-Control: public, max-age=0, must-revalidate
Accept-Ranges: bytes
Content-Range: bytes */265
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunkedKichik kuzatuv: 416 javobida ham Content-Type: text/html qolgan — kod sarlavhalarni Range tekshiruvidan oldin qo'yadi. Tana bo'sh bo'lgani uchun zarari yo'q, lekin toza kodda bu sarlavha 416 da kerak emas. Sizdagi ETag va Last-Modified boshqa chiqadi — ular fayl vaqtiga bog'liq (Date qatorini olib tashladik).
6. Path traversal: eng xavfli xato
6.1 Hujum qanday ko'rinadi
Eng sodda static server shunday yoziladi:
// ❌ XAVFLI — tekshiruvsiz
const yol = decodeURIComponent(new URL(req.url, "http://x").pathname);
createReadStream(join(ildiz, yol)).pipe(res);join("public", "/salom.txt") → public/salom.txt. Hammasi joyida. Lekin .. — "bir papka yuqoriga". join("public", "/../maxfiy.env") → maxfiy.env, ya'ni public/ dan tashqarida. Bu hujum path traversal (yo'l bo'ylab chiqish) deyiladi: URL orqali papkalar daraxtida yuqoriga chiqib, berilmasligi kerak bo'lgan faylni olish.
"new URL() .. ni o'zi tozalaydi-ku" deb o'ylash mumkin. Qisman rost. Keling, brauzerda tekshiramiz:
const yollar = [
"/../maxfiy.env",
"/%2e%2e/maxfiy.env",
"/..%2fmaxfiy.env",
"/..%5cmaxfiy.env",
];
for (const y of yollar) {
const p = new URL(y, "http://x").pathname;
console.log(p.padEnd(18), "→", decodeURIComponent(p));
}Konsolda:
/maxfiy.env → /maxfiy.env
/maxfiy.env → /maxfiy.env
/..%2fmaxfiy.env → /../maxfiy.env
/..%5cmaxfiy.env → /..\maxfiy.envBirinchi ikkitasini URL tozaladi: .. va kodlangan %2e%2e (nuqtalar) yo'qoldi. Lekin %2f — kodlangan /. URL uni papka ajratuvchisi deb hisoblamaydi va tegmaydi. Keyin biz o'zimiz decodeURIComponent qilamiz va /../maxfiy.env hosil bo'ladi. To'rtinchisi — %5c, ya'ni \. Windows'da bu ham papka ajratuvchisi.
6.2 O'lchov: fayl o'qildi
Kanonda public/salom.txt va papkadan tashqarida maxfiy.env (ichida namuna qator) bilan ikki server sinaldi. curl --path-as-is — curl'ning o'zi .. ni tozalamasligi uchun. Natija (Node 24.21.0, Windows 11). O'ng ustunda DB_PAROL so'zini qidiring — u chiqqan har qator muvaffaqiyatli hujum:
=== faqat join() (tekshiruvsiz) ===
curl --path-as-is /salom.txt → "salom\n"
curl --path-as-is /../maxfiy.env → "yo'q\n"
curl --path-as-is /%2e%2e/maxfiy.env → "yo'q\n"
curl --path-as-is /..%2fmaxfiy.env → "DB_PAROL=namuna-parol-emas\n"
curl --path-as-is /..%5cmaxfiy.env → "DB_PAROL=namuna-parol-emas\n"
=== resolve + startsWith tekshiruvi ===
curl --path-as-is /salom.txt → "salom\n"
curl --path-as-is /../maxfiy.env → "yo'q\n"
curl --path-as-is /%2e%2e/maxfiy.env → "yo'q\n"
curl --path-as-is /..%2fmaxfiy.env → "yo'q\n"
curl --path-as-is /..%5cmaxfiy.env → "yo'q\n"Tekshiruvsiz server ikki hujumda parol faylini berib yubordi. Birinchi ikkitasida himoyalangandek ko'rindi — lekin bu URL ning xizmati, bizniki emas. Haqiqiy loyihada bu fayl .env, package.json, SSH kalit yoki ma'lumotlar bazasi zaxirasi bo'lishi mumkin. %5c hujumi Windows'ga xos; Linux'da \ oddiy belgi, lekin %2f u yerda ham ishlaydi (Linux'da biz sinamadik).
6.3 Himoya: resolve va startsWith
Qoida: yo'lni to'liq hisoblang, keyin ildiz ichida ekanini tekshiring. resolve() barcha .. larni hisoblab, mutlaq yo'l qaytaradi (path darsi):
import { join, resolve, sep } from "node:path";
const ildiz = resolve("public");
function ichidami(yol) {
const fayl = resolve(join(ildiz, yol));
return fayl === ildiz || fayl.startsWith(ildiz + sep);
}
const yollar = ["/salom.txt", "/css/asosiy.css", "/../maxfiy.env",
"/rasm/../../maxfiy.env", "/../public-eski/x.txt"];
for (const y of yollar) console.log(y.padEnd(24), ichidami(y));Konsolda:
/salom.txt true
/css/asosiy.css true
/../maxfiy.env false
/rasm/../../maxfiy.env false
/../public-eski/x.txt falseOxirgi qatorga qarang. Nega startsWith(ildiz) emas, startsWith(ildiz + sep)? Chunki …/public-eski ham …/public bilan boshlanadi. Ajratuvchisiz tekshiruv qo'shni public-eski papkasini "ichida" deb o'tkazib yuborardi. sep — Windows'da \, Linux'da /.
vazifalar-api dagi statikBer yana ikki holatni ushlaydi:
- Buzuq kodlash —
/%E0%A4%A(%dan keyin yetarli belgi yo'q).decodeURIComponentURIErrortashlaydi. Ushlamasak — 500 xato; bizfalseqaytaramiz va 404 bo'ladi. - Nol bayt —
%00. Ba'zi tizimlar satrni shu yerda kesadi (maxfiy.env%00.png). Node fayl yo'lida nol baytni o'zi rad etadi, lekin oldindan tekshirish — ikkinchi qavat himoya.
Diqqat: "
..so'zini qidirib, bo'lsa rad etaman" — yetarli emas. Kodlangan variantlar (%2e%2e,..%2f,%252e— ikki marta kodlangan) va OS farqlari bor. Qora ro'yxat doim nimanidir o'tkazib yuboradi. Natijaviy yo'lni tekshirish — oq ro'yxat yondashuvi.
Tekshirib ko'ring: Ildiz
C:\app\public. So'rov/..\public2\kalit.pemyo'liga aylandi.startsWith(ildiz)nima qaytaradi vastartsWith(ildiz + sep)chi?
Javob
resolve natijasi — C:\app\public2\kalit.pem. startsWith("C:\app\public") — true (satr shunday boshlanadi), ya'ni hujum o'tadi. startsWith("C:\app\public\") — false, hujum to'xtaydi. Ajratuvchi papka nomining chegarasini belgilaydi.
7. Hujumchi nigohi
Static server — skanerlarning birinchi nishoni. Avtomatik botlar har ochiq serverga yuzlab yo'llarni sinaydi: /.env, /.git/config, /../../etc/passwd, /..%2f..%2fwindows/win.ini.
| Hujum | Natija (himoyasiz) | Bizning himoya |
|---|---|---|
/..%2fmaxfiy.env |
.env o'qiladi |
resolve + startsWith(ildiz + sep) |
/.env (public/ ichida tasodifan qolgan) |
o'qiladi | public/ ga faqat ommaviy fayllar; .gitignore emas, alohida papka |
/%E0%A4%A |
500 va stack trace | decodeURIComponent ni try/catch |
Ulkan Range so'rovlari |
bitta faylni ko'p bo'laklab o'qitish | faqat bitta oraliq, aks holda 416 |
/public/../src/server.ts |
manba kodi | ildizdan chiqish taqiqlangan |
Ikkinchi qatorga alohida e'tibor bering. Traversal himoyasi public/ dan chiqishni to'xtatadi. Lekin public/ ichiga o'zingiz .env yoki zaxira faylni tashlab qo'ysangiz, uni hech narsa himoya qilmaydi. Static papka — faqat hamma ko'rishi mumkin bo'lgan fayllar uchun.
8. API qadami: static fayllar va holat 05 (commit)
createServer, qo'lda routing va tana o'qish darslarida vazifalar-api ning Express'siz prototipi tayyor bo'ldi. Bugun unga public/ papka va src/statik.ts qo'shamiz. Holat 05 shu bilan yakunlanadi — commit shu darsda.
public/index.html — kichik bosh sahifa (kanon, 265 bayt; uzun qator ataylab bo'linmagan — ETag va Range misollari aynan shu hajmga tayanadi):
<!doctype html>
<html lang="uz">
<head><meta charset="utf-8"><title>vazifalar-api</title></head>
<body>
<h1>vazifalar-api</h1>
<p>REST API: <code>GET /vazifalar</code>, <code>POST /vazifalar</code>, <code>GET|PATCH|DELETE /vazifalar/:id</code>.</p>
</body>
</html>src/statik.ts — diagrammadagi tartibni bajaradigan statikBer (kanon fayli to'liq; uzun qatorlar telefon uchun bo'lingan, kod ma'nosi aynan):
// statik.ts — public/ papkadan fayl berish: MIME, ETag/304,
// Range/206, traversal himoyasi
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { extname, join, resolve, sep } from "node:path";
import { pipeline } from "node:stream/promises";
import type { IncomingMessage, ServerResponse } from "node:http";
const MIME: Record<string, string> = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".ico": "image/x-icon",
".txt": "text/plain; charset=utf-8",
};
/** @returns true — fayl berildi (yoki 304/206/416);
* false — bunday fayl yo'q (keyingi ishlovchiga) */
export async function statikBer(
req: IncomingMessage, res: ServerResponse,
papka: string, yol: string,
): Promise<boolean> {
const ildiz = resolve(papka);
let nomi: string;
try {
nomi = decodeURIComponent(yol);
} catch {
return false; // %E0%A4%A — noto'g'ri kodlash
}
if (nomi.includes("\0")) return false;
const fayl = resolve(join(ildiz,
nomi.endsWith("/") ? nomi + "index.html" : nomi));
// Traversal himoyasi: /../package.json kabi yo'l ildizdan chiqmasin
if (fayl !== ildiz && !fayl.startsWith(ildiz + sep)) return false;
const st = await stat(fayl).catch(() => undefined);
if (!st?.isFile()) return false;
const etag = `W/"${st.size.toString(16)}-${
Math.floor(st.mtimeMs).toString(16)}"`;
res.setHeader("Content-Type",
MIME[extname(fayl).toLowerCase()] ?? "application/octet-stream");
res.setHeader("ETag", etag);
res.setHeader("Last-Modified", st.mtime.toUTCString());
res.setHeader("Cache-Control",
"public, max-age=0, must-revalidate");
res.setHeader("Accept-Ranges", "bytes");
if (req.headers["if-none-match"] === etag) {
res.writeHead(304).end();
return true;
}
let bosh = 0;
let oxir = st.size - 1;
const range = req.headers.range;
if (range !== undefined) {
// Faqat bitta oraliq: bytes=0-99, bytes=100-, bytes=-50
const m = /^bytes=(\d*)-(\d*)$/.exec(range);
const a = m?.[1] ?? "";
const b = m?.[2] ?? "";
if (m && (a !== "" || b !== "")) {
if (a === "") {
bosh = Math.max(0, st.size - Number(b));
} else {
bosh = Number(a);
if (b !== "") oxir = Math.min(Number(b), st.size - 1);
}
}
if (!m || (a === "" && b === "") || bosh > oxir
|| bosh >= st.size) {
res.writeHead(416,
{ "Content-Range": `bytes */${String(st.size)}` }).end();
return true;
}
res.writeHead(206, {
"Content-Range":
`bytes ${String(bosh)}-${String(oxir)}/${String(st.size)}`,
"Content-Length": oxir - bosh + 1,
});
} else {
res.writeHead(200, { "Content-Length": st.size });
}
if (req.method === "HEAD") {
res.end();
return true;
}
await pipeline(
createReadStream(fayl, { start: bosh, end: oxir }), res);
return true;
}Funksiya boolean qaytaradi: true — javob berildi, false — "bunday fayl yo'q, keyingisiga o'tkaz". / bilan tugagan yo'lga index.html qo'shiladi — shuning uchun GET / bosh sahifani beradi.
src/ilova.ts ga ikki o'zgarish. Fayl boshiga import:
import { statikBer } from "./statik.ts";Va oxirgi throw dan oldin — API marshrutlari tekshirilgandan keyin — bitta qator:
if ((metod === "GET" || metod === "HEAD")
&& (await statikBer(req, res, publicPapka, url.pathname))) return;
throw new HttpXato(404, `${url.pathname} topilmadi`);Tartib muhim: agar public/vazifalar degan fayl bo'lsa ham, /vazifalar API'ga boradi. HEAD — GET bilan bir xil sarlavhalar, lekin tanasiz (HTTP metodlari darsi).
Kanondagi jonli curl (holat 05; Date qatori olib tashlangan). Bu HEAD so'rovi — javobda faqat sarlavhalar bo'ladi, ETag va Content-Length ga qarang:
curl -I $A/HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
ETag: W/"109-1a16bdb0c32"
Last-Modified: Sat, 24 Oct 2026 03:01:20 GMT
Cache-Control: public, max-age=0, must-revalidate
Accept-Ranges: bytes
Content-Length: 265
Connection: keep-alive
Keep-Alive: timeout=5curl -I — HEAD so'rovi: hamma sarlavha bor, tana yo'q. Traversal urinishi:
curl -i --path-as-is $A/%2e%2e%2fpackage.jsonHTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Content-Length: 43
Connection: keep-alive
Keep-Alive: timeout=5
{"xato":"/%2e%2e%2fpackage.json topilmadi"}package.json berilmadi — 404. Endi test/api.test.ts ga beshinchi test (kanon; qatorlar bo'lingan):
test("statik: index.html, ETag → 304, Range → 206, "
+ "traversal → 404",
async () => {
const r = await fetch(`${asos}/`);
assert.equal(r.status, 200);
assert.match(r.headers.get("content-type") ?? "", /text\/html/);
const etag = r.headers.get("etag") ?? "";
await r.text();
assert.equal((await fetch(`${asos}/`,
{ headers: { "If-None-Match": etag } })).status, 304);
const q = await fetch(`${asos}/index.html`,
{ headers: { Range: "bytes=0-14" } });
assert.equal(q.status, 206);
assert.equal(await q.text(), "<!doctype html>");
assert.equal((await fetch(`${asos}/index.html`,
{ headers: { Range: "bytes=99999-" } })).status, 416);
// fetch URL'ni normallashtiradi (/../ yo'qoladi) — kodlangan
// nuqtalar bilan
assert.equal(
(await fetch(`${asos}/%2e%2e/package.json`)).status, 404);
assert.equal(
(await fetch(`${asos}/..%2fpackage.json`)).status, 404);
});Test fetch bilan ham 304 oladi: fetch bu so'rovga o'zi Cache-Control: no-cache qo'shadi, lekin bizning statikBer bu sarlavhaga qaramaydi. Express'ga o'tgach bu farq testni yiqitadi — req darsida ko'rasiz.
Holat 05 tekshiruvi (kanon, Node 24.21.0): npm run tip 0 xato, npm run lint 0, testlar 5/5 (oldingi darsdagi 4 ta + static). Prod bog'liqlik — 0, hammasi node:* modullarida. Sovuq start — 201 ms (node src/server.ts, type stripping). Oddiy yuk (autocannon, 10 ulanish × 5 s, server va yuk beruvchi bir mashinada — bitta o'lchov): GET /vazifalar (100 vazifa, 26 KB) — 2 644 req/s, GET /vazifalar/5 — 25 611 req/s. Bu raqamlarni eslab qoling: Express'ga o'tgach xuddi shu o'lchovni takrorlaymiz.
Commit:
git add .
git commit -m "vazifalar-api v0: node:http — CRUD, qo'lda routing, JSON tana (413/415/400), public/ statik fayllar"9. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| CSS qo'llanmayapti, konsolda "MIME type ('text/plain')" | Noto'g'ri Content-Type |
MIME jadvali, charset=utf-8 |
| Rasm o'rniga yuklab olish oynasi | Noma'lum kengaytma → octet-stream |
Jadvalga qo'shing |
| Server xotirasi video bilan to'ladi | readFile |
createReadStream + pipeline |
| Video surilmaydi | Range qo'llanmaydi yoki Accept-Ranges yo'q |
206 + Content-Range |
| Range javobi bir bayt kam/ko'p | end ichiga olishini unutish |
oxir - bosh + 1 |
URIError: URI malformed → 500 |
decodeURIComponent ushlanmagan |
try/catch → 404 |
.env internetdan o'qiladi |
join tekshiruvsiz yoki startsWith(ildiz) sepsiz |
resolve + startsWith(ildiz + sep) |
URIError: URI malformed — "URI noto'g'ri tuzilgan". % belgisidan keyin ikkita o'n oltilik raqam kutiladi; %A yoki %E0%A4%A kabi kesik ketma-ketlik shu xatoni beradi.
10. Mashqlar
Yechimlar kurs/mashqlar/25/05-statik/ da.
1-mashq (oson): to'ldiring
- Fayl o'zgarmagan bo'lsa server qaytaradigan status —
- Brauzer keshdagi versiyani yuboradigan sarlavha —
- Faylning bir qismi qaytarilganda status —
- Range fayldan tashqarida bo'lsa status —
- Noma'lum kengaytma uchun xavfsiz MIME turi —
application/
Yechim
- 304 Not Modified — tanasiz javob, brauzer keshdagini ishlatadi.
- If-None-Match — ichida server avval bergan ETag.
- 206 Partial Content —
Content-Rangebilan. - 416 Range Not Satisfiable —
Content-Range: bytes */<hajm>bilan. - octet-stream — "noma'lum baytlar", brauzer ochmaydi, yuklab oladi.
2-mashq (o'rta): Content-Range yasash
«Tahlil — toza funksiya» bo'limidagi rangeOl natijasidan javob sarlavhalarini yasaydigan rangeSarlavhalari(oraliq, hajm) funksiyasini yozing. U { status, sarlavhalar } qaytarsin: oraliq undefined → 200 va Content-Length; null → 416 va Content-Range: bytes */<hajm>; aks holda 206, Content-Range va Content-Length. Toza JS — saytdagi «Ishga tushir» bilan sinang. Eslatma: Content-Length = oxir - bosh + 1.
Yechim
function rangeSarlavhalari(oraliq, hajm) {
if (oraliq === undefined) {
return { status: 200, sarlavhalar: { "Content-Length": hajm } };
}
if (oraliq === null) {
const sarlavhalar = { "Content-Range": `bytes */${hajm}` };
return { status: 416, sarlavhalar };
}
const { bosh, oxir } = oraliq;
return {
status: 206,
sarlavhalar: {
"Content-Range": `bytes ${bosh}-${oxir}/${hajm}`,
"Content-Length": oxir - bosh + 1,
},
};
}
console.log(JSON.stringify(rangeSarlavhalari(undefined, 265)));
console.log(JSON.stringify(rangeSarlavhalari(null, 265)));
console.log(JSON.stringify(
rangeSarlavhalari({ bosh: 257, oxir: 264 }, 265)));Konsolda:
{"status":200,"sarlavhalar":{"Content-Length":265}}
{"status":416,"sarlavhalar":{"Content-Range":"bytes */265"}}
{"status":206,"sarlavhalar":{"Content-Range":"bytes 257-264/265","Content-Length":8}}Uchinchi holat — kanondagi curl ... -H "Range: bytes=-8" javobi bilan bir xil: bytes 257-264/265, 8 bayt.
3-mashq (qiyin): xavfsiz yo'l
xavfsizYol(ildiz, urlYol) funksiyasini yozing (Node, node:path). U urlYol ni dekodlasin, buzuq kodlashda va nol baytda null qaytarsin, / bilan tugasa index.html qo'shsin, ildizdan chiqsa null, aks holda ildizga nisbatan yo'lni qaytarsin (relative bilan, Windows'da ham / bilan). relative(a, b) — a dan b ga nisbiy yo'l (path darsi).
Yechim
import { join, relative, resolve, sep } from "node:path";
function xavfsizYol(ildiz, urlYol) {
const asos = resolve(ildiz);
let nomi;
try {
nomi = decodeURIComponent(urlYol);
} catch {
return null; // buzuq kodlash
}
if (nomi.includes("\0")) return null;
if (nomi.endsWith("/")) nomi += "index.html";
const fayl = resolve(join(asos, nomi));
if (fayl !== asos && !fayl.startsWith(asos + sep)) return null;
return relative(asos, fayl).split(sep).join("/");
}
const sinovlar = ["/", "/css/asosiy.css", "/..%2fmaxfiy.env",
"/%E0%A4%A", "/rasm.png%00.txt", "/a/./b/../c.txt"];
for (const s of sinovlar) {
console.log(s.padEnd(18), xavfsizYol("public", s));
}Konsolda:
/ index.html
/css/asosiy.css css/asosiy.css
/..%2fmaxfiy.env null
/%E0%A4%A null
/rasm.png%00.txt null
/a/./b/../c.txt a/c.txtOxirgi qator: . va .. ildiz ichida qolsa — ruxsat, resolve ularni hisoblab qo'yadi. Biz yo'lni taqiqlamaymiz, faqat natija qayerga tushishini tekshiramiz.
4-mashq: API qadami
vazifalar-api ga public/index.html va src/statik.ts ni qo'shing (MIME jadvali, ETag, Last-Modified, Cache-Control, Accept-Ranges, 304, 206/416, HEAD, traversal himoyasi). ilova.ts da uni API marshrutlaridan keyin chaqiring. Testga qo'shing: / → 200 text/html; ETag bilan → 304; Range: bytes=0-14 → 206 va <!doctype html>; bytes=99999- → 416; /%2e%2e/package.json va /..%2fpackage.json → 404. npm run check yashil bo'lsa — commit («API qadami» bo'limidagi xabar bilan).
Yechim
Namuna — «API qadami» bo'limidagi statikBer va ilova.ts qatorlari; Range qismi — «Tahlil — toza funksiya» bo'limidagi rangeOl mantiqi. Testda fetch URL'dagi /../ ni o'zi tozalaydi, shuning uchun traversal'ni faqat kodlangan nuqta yoki %2f bilan sinash mumkin (/%2e%2e/package.json, /..%2fpackage.json). Kodlanmagan /../ ni sinash uchun node:http ning request i yoki curl --path-as-is kerak.
11. Real ishda
- Prodakshnda static fayllarni ko'pincha Node bermaydi. Ularni nginx yoki CDN (Cloudflare, CloudFront) beradi — tezroq va Node'ning event loop'ini band qilmaydi. Reverse proxy darsida bu sxemani ko'rasiz.
- Express'da — bitta qator:
app.use(express.static("public")). U ichida aynan shu bilimlarni ishlatadi: MIME, ETag, 304, Range, traversal himoyasi. Express'ga kirish darsida bizningstatik.tso'chiriladi. - Fayl yuklash xizmatlari (rasm, PDF, video kurslar) — Range va 206 albatta kerak.
- Xavfsizlik auditida path traversal (CWE ro'yxatida 22-raqam) — eng ko'p topiladigan zaifliklardan biri. Intervyuda: "static server qanday himoyalanadi?", "ETag va Last-Modified farqi?", "206 qachon qaytadi?".
Xulosa
Content-Type— kengaytmadan, kichik harf bilan; matngacharset=utf-8; noma'lum —application/octet-stream.- Faylni
pipeline(createReadStream(fayl), res)bilan bering; hajmnistat()danContent-Lengthga yozing. ETag(hajm + vaqt) vaIf-None-Match→304, tanasiz.Range→206vaContent-Range; fayldan tashqarida →416;endichiga oladi.- Traversal:
new URL..%2fni to'xtatmaydi. Faqatresolve()+startsWith(ildiz + sep)ishonchli; buzuq%kodlashni ushlang.
Keyingi dars: HTTPS, TLS va HTTP/2 — sertifikat, https.createServer, ALPN va nega prodakshnda TLS'ni Node emas, proxy bajaradi.
Manbalar
- RFC 9110 "HTTP Semantics" (2022-06) — Range Requests, 206, 416, ETag,
If-None-Match, 304: https://www.rfc-editor.org/rfc/rfc9110 - MDN — HTTP range requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests (2026-10-24 da ochildi)
- OWASP — Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal
- Node.js 24 hujjati —
fs.createReadStream(start/endichiga oladi),stream.pipeline: https://nodejs.org/docs/latest-v24.x/api/fs.html - Kanon:
vazifalar-apiholat 05 (statik.ts, testlar, jonli curl, yuk o'lchovi), traversal tajribasi. 304, 206 (bytes=-8), 416 curl chiqishlari — biz ishga tushirdik (Node 24.21.0, curl 8.19.0, Windows 11).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!