Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Yarim yozilgan fayl: tajriba
- 2.1 Kanon tajribasi
- 2.2 Natijani o'qiymiz
- 2.3 Halol izoh
- 3. Atomik yozish
- 3.1 Nega rename "atomik"
- 3.2 atomikYoz funksiyasi
- 3.3 Chegara 1: fsync
- 3.4 Chegara 2: Windows'da EPERM
- 4. Ochiq fayllar tugashi: EMFILE
- 4.1 Biz ishga tushirdik
- 4.2 Qayerdan keladi
- 5. Path traversal
- 5.1 Hujum
- 5.2 Himoya: resolve + "ichidami?"
- 5.3 Nega + p.sep kerak
- 5.4 Zip slip
- 6. Hujumchi nigohi
- 7. CLI qadami: fayl.ts va atomik kesh (holat 18, 3-qism)
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): ichidami
- 3-mashq (qiyin): arxiv ochuvchi tekshiruvi
- 4-mashq: CLI qadami
- 10. Real ishda
- Xulosa
- Manbalar
Xavfsiz fayl operatsiyalari: atomik yozish, EMFILE va path traversal
Qisqacha: Uchta klassik fayl xatosi bor. Yarim yozilgan fayl: yozish o'rtasida dastur o'lsa, fayl buziladi — bizning tajribada oddiy yozish 20 dan 17 marta buzilgan JSON qoldirdi. Davosi — avval vaqtinchalik faylga yozib, keyin
renameqilish (20 dan 0). Ochiq fayllar tugashi (EMFILE): bir vaqtda minglab fayl ochilsa, OS rad etadi — Windows'da 8 189 tadan keyin. Path traversal: foydalanuvchi bergan../../nom sizning papkangizdan chiqib ketadi —path.resolve+ "papka ichidami?" tekshiruvi bilan to'xtatiladi.
Bu darsda
- Atomik yozish nima ekanini va nega
renameuni ta'minlashini tushuntira olasiz; o'lchov natijasini o'qiysiz. atomikYozfunksiyasini yozasiz va uning chegaralarini (fsync, WindowsEPERM) bilasiz.EMFILEqachon chiqishini va undan qanday saqlanishni bilasiz.../path traversal va zip slip hujumlarini tushuntirib, ularga qarshi tekshiruv yoza olasiz.
Oldin bilishingiz kerak: fs: uch API shakli, fs chuqur: kataloglar, stat, glob va watch, process: exit kodlari va signallar.
1. Nega bu kerak?
valyuta keshga yozayotgan paytda foydalanuvchi Ctrl+C bosdi. Yoki noutbuk batareyasi tugadi. Yoki Windows yangilanish uchun kompyuterni qayta yoqdi. Kesh faylida nima qoladi?
Ko'pchilik "eski fayl yoki yangi fayl" deb javob beradi. Afsuski, uchinchi variant bor: yarmi. Fayl boshida yangi ma'lumot, oxiri yo'q. JSON.parse uni o'qiy olmaydi. Kesh uchun bu kichik muammo — o'tgan darsda buzilgan keshni "yo'q" deb hisoblashni o'rgandik. Lekin xuddi shu xato foydalanuvchining sozlama faylida, buxgalteriya hisobotida yoki o'yin saqlanmasida bo'lsa — ma'lumot yo'qoladi.
Bu darsda yana ikkita "ko'rinmas" xatoni ko'ramiz. Ular oddiy sinovda chiqmaydi, faqat yuklama ostida yoki hujumchi qo'lida paydo bo'ladi. Shuning uchun ular intervyuda ko'p so'raladi va haqiqiy hodisalarga sabab bo'ladi.
O'xshatish: devorga e'lon osish. Eski e'lonni yirtib, yangisini yozishni boshlasangiz va o'sha payt sizni chaqirib ketishsa — devorda yarim yozilgan qog'oz qoladi. To'g'ri usul: yangi e'lonni stolda to'liq yozasiz, keyin bir harakatda eskisining o'rniga osasiz. Har qanday paytda devorda butun e'lon turadi — eskisi yoki yangisi.
2. Yarim yozilgan fayl: tajriba
2.1 Kanon tajribasi
Buzilish "kamdan-kam" bo'ladi — shuning uchun uni ko'rish uchun ataylab qo'zg'atish kerak. Kanon tajribasi shunday qurilgan:
- 30 MB JSON (600 000 yozuv) tayyorlanadi.
- Alohida jarayon uni faylga 64 KB bo'laklab yozadi.
- Asosiy dastur yozuvchini har safar boshqa paytda (80 ms, 95 ms, 110 ms …)
SIGKILLbilan o'ldiradi — elektr o'chishiga eng yaqin taqlid: jarayon hech narsani tugata olmaydi. - Keyin faylni
JSON.parsebilan o'qib ko'radi: buzilgan, eski yoki to'liq yangi. - 20 marta oddiy yozish, 20 marta "vaqtinchalik fayl + rename".
Yozuvchi jarayonning asosiy qismi (to'liq skript uzun, faqat g'oya):
// "w" — faylni ochilishi bilan 0 baytga qisqartiradi
const fd = fs.openSync(fayl, "w");
for (let o = 0; o < d.length; o += 65536) {
// 64 KB bo'lak
fs.writeSync(fd, d, o, Math.min(65536, d.length - o));
}
fs.closeSync(fd);
// atomik variantda: fayl = "b.json.tmp", oxirida
// fs.renameSync("b.json.tmp", "b.json")Natija (Windows 11, Node 24.21.0, i5-12500H):
oddiy : 20 urinish — buzilgan JSON 17, eski 0, to'liq yangi 3
atomik: 20 urinish — buzilgan JSON 0, eski 17, to'liq yangi 3Rasmda farq yanada aniq:
- oddiy writeFileeski ma'lumot ham yo'qoldi17 ta
- tmp + rename17 eski, 3 yangi0 ta
Manba: IlmHamroh kanon tajribasi t17-atomik: 30 MB JSON, SIGKILL, Node 24.21.0, Windows 11, i5-12500H, 2026-10-23
2.2 Natijani o'qiymiz
Ikkala qatorda "to'liq yangi — 3": yozish tugagandan keyin o'ldirilgan uchta urinish. Qolgan 17 tasida yozish o'rtasida to'xtatildi. Farq — shu 17 ta urinishda nima qoldi:
- Oddiy yozish: 17 ta buzilgan JSON va 0 ta eski.
"w"rejimi faylni ochilishi bilanoq bo'shatadi. Demak eski ma'lumot birinchi millisekunddayoq yo'qoldi, yangisi esa yarim qoldi. Ikki ma'lumotdan ham ayrildik. - Atomik yozish: 0 ta buzilgan, 17 ta eski. Yozish vaqtinchalik faylga ketdi, asl fayl tegmadi.
renamegacha yetib bormadi — asl faylda eski, butun ma'lumot qoldi.
Atomik yozish yangi ma'lumotni saqlab qololmaydi — u yozib bo'linmagan. Lekin u kafolat beradi: fayl har doim butun. Eski butun kesh — ishlaydi, buzilgan yangi kesh — ishlamaydi.
2.3 Halol izoh
Kanon tajribaning birinchi variantida bitta writeFileSync bilan 20 marta urindi va bitta ham buzilish chiqmadi. Sabab — yozish oynasi juda qisqa edi: Node faylni bir zumda yozib ulgurdi. Shundan keyin yozuvchi 64 KB bo'laklarga va har bo'lak orasida 1 ms tanaffusga o'tkazildi. Bu shunchaki oynani kengaytiradi, hodisani o'ylab topmaydi: katta fayl yoki sekin disk (USB, tarmoq diski) haqiqiy hayotda xuddi shunday oynani beradi.
Ya'ni: kichik faylda buzilish ehtimoli kichik, lekin nolga teng emas. Va u eng yomon paytda bo'ladi.
Tekshirib ko'ring: Oddiy yozishda nega "eski" natija bitta ham chiqmadi?
Javob
openSync(fayl, "w") faylni ochilishi bilanoq 0 baytga qisqartiradi. Undan keyingi har qanday paytda o'ldirilsa, faylda eski ma'lumot yo'q — faqat yangisining boshi. "Eski" holat faqat open dan oldin o'ldirilganda bo'lardi, bu oyna esa juda qisqa.
3. Atomik yozish
3.1 Nega rename "atomik"
Atomik (atomic) — "bo'linmas": amal yo butunlay bajariladi, yo umuman bajarilmaydi, o'rtadagi holat ko'rinmaydi. Bu so'zni SharedArrayBuffer va Atomics mavzusida ham uchratgansiz — ma'nosi bir xil.
Faylga yozish atomik emas: u ko'p bo'lakdan iborat. rename esa bir papka ichida — fayl tizimidagi bitta yozuvni boshqa joyga ko'chirish. Ma'lumot baytlari ko'chmaydi, faqat "bu nom endi shu ma'lumotga ishora qiladi" degan yozuv almashadi. POSIX standarti (Linux, macOS) bir fayl tizimi ichidagi rename atomik ekanini kafolatlaydi. Windows'da Node MoveFileEx ni almashtirish rejimida chaqiradi — amalda ham shunday ishlaydi, lekin pastda bitta muhim istisnoni ko'ramiz.
Shart: vaqtinchalik fayl o'sha papkada bo'lsin. Boshqa diskdagi os.tmpdir() dan rename — bu ko'chirish (nusxa + o'chirish), atomik emas. Ba'zan esa EXDEV ("cross-device link") xatosi beradi.
3.2 atomikYoz funksiyasi
Kanon src/fayl.ts dagi funksiyaning JS ko'rinishi:
import { randomBytes } from "node:crypto";
import {
mkdir, readdir, readFile, rename, rm, writeFile,
} from "node:fs/promises";
import path from "node:path";
async function atomikYoz(fayl, malumot) {
await mkdir(path.dirname(fayl), { recursive: true });
const tasodif = randomBytes(4).toString("hex");
const vaqtinchalik = `${fayl}.${process.pid}.${tasodif}.tmp`;
try {
await writeFile(vaqtinchalik, malumot, { mode: 0o600 });
await rename(vaqtinchalik, fayl);
} catch (xato) {
await rm(vaqtinchalik, { force: true });
throw xato;
}
}
await rm("kesh", { recursive: true, force: true });
await atomikYoz("kesh/kurslar.json", '[{"Ccy":"USD"}]');
await atomikYoz("kesh/kurslar.json", '[{"Ccy":"EUR"}]');
console.log(await readFile("kesh/kurslar.json", "utf8"));
console.log(await readdir("kesh"));Konsolda:
[{"Ccy":"EUR"}]
[ 'kurslar.json' ]Ikki marta yozildi, papkada bitta fayl — .tmp qolmadi. Har qatorning sababi:
| Qator | Nega |
|---|---|
mkdir(…, { recursive: true }) |
Papka hali yo'q bo'lishi mumkin (fs chuqur) |
process.pid + 4 tasodifiy bayt |
Ikki valyuta jarayoni bir vaqtda yozsa, vaqtinchalik fayllari to'qnashmaydi |
O'sha papka (${fayl}.….tmp) |
rename atomik bo'lishi uchun |
mode: 0o600 |
Linux/macOS: faqat egasi o'qiydi |
catch → rm(vaqtinchalik) |
Xato bo'lsa, axlat qolmasin; xato yuqoriga uzatiladi |
randomBytes — kriptografik tasodif; uni crypto 1 darsida o'rganamiz. Bu yerda faqat noyob nom uchun ishlatildi.
Jarayon SIGKILL bilan o'ldirilsa, catch ishlamaydi — .tmp fayl qoladi. Bu zararsiz: u hech qachon o'qilmaydi, keyingi valyuta kesh tozala uni o'chiradi (u papkadagi hamma faylni ro'yxatga oladi, .tmp ham kiradi).
Halol izoh: Ctrl+C. Hozirgi
valyutadaCtrl+Cham xuddi shunday ta'sir qilishi mumkin. process obyekti darsidan beribin.tsdagiSIGINTishlovchisi darholprocess.exit(130)ni chaqiradi.process.exitkutib turganawaitlarni tugatmaydi —catchham,rm(vaqtinchalik)ham ishlamaydi. NatijaSIGKILLdagidek: asl fayl butun, lekin.tmpqolishi mumkin. Biz buni sinamadik: haqiqiyCtrl+Cuchun haqiqiy terminal kerak, Windows'da esa boshqa jarayondan yuborilganSIGINTishlovchini chaqirmaydi (process obyekti darsidagi o'lchov). BirinchiCtrl+Cda ishni yumshoq to'xtatish (Built-in fetch va undici darsidan boshlab, holat 46) bu oynani kichraytiradi.
3.3 Chegara 1: fsync
writeFile tugadi — ma'lumot diskdami? Har doim emas. OS yozuvni avval xotiradagi sahifa keshiga (page cache) qo'yadi va diskka keyinroq tushiradi. Jarayon o'lsa — muammo yo'q, OS baribir yozib qo'yadi. Lekin elektr o'chsa, xotira yo'qoladi.
fsync — "kutib turgan hamma narsani hozir diskka yoz" degan tizim chaqiruvi. Node'da:
await writeFile(vaqtinchalik, malumot, { flush: true }); // Node 21+
// yoki FileHandle bilan:
await using f = await open(vaqtinchalik, "w");
await f.writeFile(malumot);
await f.sync();To'liq kafolat uchun hatto rename dan keyin papkani ham fsync qilish kerak (Linux'da papka yozuvi ham keshda turadi). Ma'lumotlar bazalari aynan shunday qiladi.
Kanonning atomikYoz i fsync qilmaydi — bu ongli qaror. U jarayon o'limidan (Ctrl+C, SIGKILL, xato) himoya qiladi, elektr o'chishidan esa to'liq emas. Kesh uchun bu yetarli: eng yomon holatda keyingi so'rov keshni qayta yozadi. Buxgalteriya fayli uchun yetarli bo'lmasdi. fsync qimmat: u diskni kutadi, ayniqsa HDD'da o'nlab millisekund.
3.4 Chegara 2: Windows'da EPERM
Biz yana bir tajriba qildik: kesh faylini bir dasta (FileHandle) bilan ochiq qoldirib, ustiga rename qildik.
import { open, rename, writeFile } from "node:fs/promises";
await writeFile("kesh.json", "eski");
const dasta = await open("kesh.json", "r"); // kimdir o'qiyapti
await writeFile("kesh.json.tmp", "yangi");
try {
await rename("kesh.json.tmp", "kesh.json");
} catch (xato) {
console.log(xato.code);
}
await dasta.close();Windows 11, Node 24.21.0 da natija:
EPERMWindows ochiq faylning ustiga rename qilishga ruxsat bermadi. Haqiqiy hayotda faylni boshqa jarayon ushlab turishi mumkin: antivirus tekshiryapti, indekslash xizmati, ikkinchi valyuta, muharrir. Linux va macOS'da bu muammo yo'q (hujjatga ko'ra; biz sinamadik).
Mashhur kutubxonalar buni qanday hal qiladi? graceful-fs (npm: 4.2.11) Windows'da rename ni EPERM/EACCES da bir necha soniya davomida qayta urinadi. write-file-atomic (8.0.0) — npm'ning o'zi ishlatadigan atomik yozish paketi. valyuta qayta urinmaydi: kesh yozilmasa, manba.ts ogohlantiradi va natijani baribir ko'rsatadi — keshga yozib bo'lmadi (…). Kesh — yordamchi narsa, unga bir soniya kutish kerak emas.
Tekshirib ko'ring: Vaqtinchalik faylni
os.tmpdir()ga (masalanC:\Users\…\AppData\Local\Temp) yozib, keyinD:\loyiha\kesh.jsongarenameqilsak, nima bo'ladi?
Javob
Ikki xil disk — rename atomik bo'la olmaydi. OS EXDEV xatosini beradi yoki (ba'zi tizimlarda va kutubxonalarda) nusxa ko'chirib, keyin o'chiradi — bu yana oddiy yozishga aylanadi va yarim fayl xavfi qaytadi. Vaqtinchalik fayl doim maqsad fayl bilan bitta papkada bo'lsin.
4. Ochiq fayllar tugashi: EMFILE
4.1 Biz ishga tushirdik
Har ochiq fayl OS'da bitta fayl deskriptori (file descriptor) — "ochiq fayl raqami" — egallaydi. Ularning soni cheklangan. Tajriba: bitta faylni 20 000 marta ochamiz va yopmaymiz.
import { open, writeFile } from "node:fs/promises";
await writeFile("a.txt", "x");
const ochiq = [];
try {
for (let i = 0; i < 20000; i++) {
ochiq.push(await open("a.txt", "r"));
}
} catch (xato) {
console.log(ochiq.length, xato.code);
console.log(xato.message);
}
await Promise.all(ochiq.map((f) => f.close()));Windows 11, Node 24.21.0:
8189 EMFILE
EMFILE: too many open files, open 'C:\Users\…\a.txt'"Ochiq fayllar juda ko'p". 8 189 — Windows'dagi chegara (8 192 dan uchtasi stdin, stdout, stderr uchun band). Linux'da sukut bo'yicha chegara ko'pincha 1 024 (ulimit -n), ya'ni xato ancha tezroq chiqadi (hujjatga ko'ra; biz Linux'da sinamadik).
4.2 Qayerdan keladi
Hech kim ataylab 20 000 fayl ochmaydi. Xato odatda ikki yo'l bilan keladi:
- Yopilmagan dastalar.
openqilindi,closeunutildi — yokiclosedan oldin xato chiqdi. Har so'rovda bitta oqish — server bir necha soatdan keyin yiqiladi. Davosi —await usingyokitry/finally(fs chuqur). - Haddan tashqari parallellik.
Promise.all(fayllar.map(readFile))— 50 000 fayl bo'lsa, hammasi bir vaqtda ochiladi.
Ikkinchisining davosi — bir vaqtda ishlaydigan amallar sonini cheklash. Oddiy usul — bo'laklab:
import { readFile, writeFile, rm, mkdir } from "node:fs/promises";
async function bolaklabOqi(fayllar, hajm) {
const natija = [];
for (let i = 0; i < fayllar.length; i += hajm) {
const bolak = fayllar.slice(i, i + hajm);
const matnlar = bolak.map((f) => readFile(f, "utf8"));
natija.push(...(await Promise.all(matnlar)));
}
return natija;
}
await rm("ombor", { recursive: true, force: true });
await mkdir("ombor");
const nomlar = [];
for (let i = 0; i < 250; i++) {
nomlar.push(`ombor/${i}.txt`);
await writeFile(`ombor/${i}.txt`, String(i));
}
const matnlar = await bolaklabOqi(nomlar, 100);
console.log(matnlar.length, matnlar.at(-1));Konsolda:
250 249250 fayl — 100, 100 va 50 dan uch bo'lakda. Bir vaqtda ko'pi bilan 100 ta fayl ochiq. Professional kodda buning uchun p-limit kabi kichik kutubxonalar ishlatiladi. Ular bo'lak tugashini kutmaydi: bittasi tugashi bilan keyingisini boshlaydi.
graceful-fs boshqa yo'l tutadi: EMFILE chiqsa, amalni navbatga qo'yadi va qaysidir fayl yopilganda qayta urinadi. npm, webpack va ko'p eski asboblar uning ustida ishlaydi. Lekin u sababni emas, belgini davolaydi — oqayotgan dastalarni baribir topish kerak.
Tekshirib ko'ring: Server har so'rovda
openqiladi va javob qaytgachcloseqiladi. Lekin ba'zi so'rovlarda o'rtada xato chiqadi. Bir kundan keyinEMFILEboshlandi. Sabab nima?
Javob
Xato chiqqan so'rovlarda close ga navbat kelmagan — dasta oqqan. Har bunday so'rov bitta deskriptor "yeydi". close ni finally ga ko'chirish yoki await using ishlatish kerak: shunda xato bilan ham yopiladi.
5. Path traversal
5.1 Hujum
valyuta ga yangi imkoniyat qo'shmoqchimiz, deylik: valyuta eksport <nom> — kesh papkasiga <nom>.csv faylini yozadi. Nom foydalanuvchidan keladi. Kod oddiy:
const fayl = path.join(keshPapka, nom + ".csv"); // ❌
await writeFile(fayl, csv);Endi foydalanuvchi nom sifatida ../../../Desktop/salom beradi. path.join .. ni "bir papka yuqoriga" deb bajaradi — fayl kesh papkasidan tashqariga chiqib ketadi. Bu — path traversal (yo'l bo'ylab yurish, "katalogdan chiqib ketish"). Serverda bu yanada xavfli: GET /fayllar?nom=../../.env — va sirlar ochiq.
5.2 Himoya: resolve + "ichidami?"
To'g'ri tekshiruv ikki qadam:
path.resolve(asos, nom)— to'liq, normallashtirilgan yo'l quradi (..lar hisoblanadi).- Natija
asos + ajratgichbilan boshlanishini tekshiradi.
Misolni Linux yo'llari (path.posix) bilan yozamiz — natija hamma OS'da bir xil chiqadi:
import path from "node:path";
const p = path.posix;
function xavfsizYol(asos, nom) {
const toliq = p.resolve(asos, nom);
if (!toliq.startsWith(asos + p.sep)) {
throw new Error(`Ruxsat yo'q: ${nom}`);
}
return toliq;
}
const asos = "/home/aziz/.cache/valyuta";
const nomlar = [
"usd.csv", "2026/oktyabr.csv", "../../.ssh/id_rsa", "/etc/passwd",
];
for (const nom of nomlar) {
try {
console.log("OK ", xavfsizYol(asos, nom));
} catch (xato) {
console.log("RAD ", xato.message);
}
}Konsolda:
OK /home/aziz/.cache/valyuta/usd.csv
OK /home/aziz/.cache/valyuta/2026/oktyabr.csv
RAD Ruxsat yo'q: ../../.ssh/id_rsa
RAD Ruxsat yo'q: /etc/passwdTo'rtinchi qatorga qarang: mutlaq yo'l. path.resolve mutlaq yo'lni ko'rsa, oldingi qismlarni tashlab yuboradi. "/etc/passwd" ni join qilsangiz — asos/etc/passwd chiqardi va zararsiz ko'rinardi, resolve esa haqiqatni ko'rsatadi.
5.3 Nega + p.sep kerak
Ajratgichsiz startsWith(asos) ham "ishlayotgandek" ko'rinadi. Lekin qo'shni papka nomi asos bilan boshlansa-chi?
import path from "node:path";
const asos = "/srv/kesh";
const toliq = path.posix.resolve(asos, "../kesh-eski/sir.json");
console.log(toliq);
console.log(toliq.startsWith(asos)); // ❌ noto'g'ri tekshiruv
console.log(toliq.startsWith(asos + "/")); // ✅ to'g'riKonsolda:
/srv/kesh-eski/sir.json
true
false/srv/kesh-eski matn sifatida /srv/kesh bilan boshlanadi, lekin u boshqa papka. Bitta belgi — / — butun himoyani hal qiladi.
Windows'da yana bitta tuzoq — disk harfi va katta-kichik harf. path.win32.resolve("C:\\kesh", "D:\\boshqa") natijasi — D:\boshqa: boshqa disk, butunlay boshqa joy. Windows'da C:\Kesh va c:\kesh — bitta papka, startsWith esa ularni farqlaydi. Shuning uchun Windows'da ikkalasini ham bir xil harfga keltirib solishtirish kerak. Muqobil usul — path.relative(asos, toliq): natija .. bilan boshlansa yoki mutlaq bo'lsa — tashqarida.
Symlink (ramziy havola) ham chiqib ketish yo'li bo'lishi mumkin: papka ichidagi havola aslida /etc ga ishora qilsa, yo'l tekshiruvdan o'tadi. Juda qattiq himoya kerak bo'lsa, fs.realpath bilan haqiqiy yo'l olinib, keyin tekshiriladi.
5.4 Zip slip
Zip slip — path traversal'ning arxiv ko'rinishi (Snyk tadqiqotchilari 2018-yilda shu nom bilan e'lon qilgan, minglab loyihada topilgan). Arxiv ichidagi fayl nomlari ham shunchaki satr: ../../.bashrc nomli fayl bo'lishi mumkin. Arxivni ochadigan kod har nomni path.join(papka, nom) bilan yozsa — fayl papkadan tashqariga tushadi. Himoya o'sha: har yozuv uchun xavfsizYol. tar va yauzl kabi zamonaviy kutubxonalar buni o'zi tekshiradi, lekin o'zingiz ochadigan kod yozsangiz — mas'uliyat sizda.
Tekshirib ko'ring:
nom.includes("..")bo'lsa rad etish — yetarli himoyami?
Javob
Yo'q. Mutlaq yo'l (/etc/passwd, C:\Windows\…) .. siz ham tashqariga chiqadi. Ba'zi tizimlar URL-kodlangan (%2e%2e) yoki boshqa ko'rinishdagi nomlarni keyinroq ochadi. report..csv kabi zararsiz nom esa rad etiladi. To'g'ri yo'l — natijaviy yo'lni hisoblab, uning qayerga tushganini tekshirish.
6. Hujumchi nigohi
| Hujum | Natija | Himoya |
|---|---|---|
nom = "../../.env" |
Sir fayllar o'qiladi yoki ustiga yoziladi | resolve + startsWith(asos + sep) |
Mutlaq yo'l /etc/passwd |
Asosni chetlab o'tish | resolve uni ko'rsatadi; rad etish |
Arxivda ../ nomli fayl (zip slip) |
Arxiv ochilganda tizim fayllari almashtiriladi | Har yozuvni tekshirish |
| Juda ko'p parallel so'rov | EMFILE — server hamma uchun yiqiladi (DoS) |
Parallellik chegarasi, dastalarni yopish |
| Bir vaqtda ikki yozuvchi | Biri ikkinchisini yarim yozilgan faylga almashtiradi | Atomik yozish, noyob .tmp nomlar |
| Ochiq qolgan vaqtinchalik fayllar | Boshqa foydalanuvchi o'qiydi | mode: 0o600, o'z papkangiz |
valyuta hozircha foydalanuvchi bergan nom bilan fayl yaratmaydi — kesh nomlari kod ichida quriladi. Lekin tarix eksporti (-o fayl) qo'shilganda shu darsning qoidalari kerak bo'ladi.
7. CLI qadami: fayl.ts va atomik kesh (holat 18, 3-qism)
Holat 18 ning uchinchi qismi. Hali commit qilmang — commit path va os darsida.
src/fayl.ts— kanonatomikYoz(TypeScript ko'rinishi):
// Xavfsiz yozish: avval vaqtinchalik faylga, keyin rename
// (bir xil papkada — atomik). Yarim yozilgan kesh hech qachon o'qilmaydi
import { randomBytes } from "node:crypto";
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
export async function atomikYoz(fayl: string, malumot: string | Uint8Array): Promise<void> {
await mkdir(path.dirname(fayl), { recursive: true });
const vaqtinchalik = `${fayl}.${String(process.pid)}.${randomBytes(4).toString("hex")}.tmp`;
try {
await writeFile(vaqtinchalik, malumot, { mode: 0o600 });
await rename(vaqtinchalik, fayl);
} catch (xato) {
await rm(vaqtinchalik, { force: true });
throw xato;
}
}Birinchi izoh qatorini telefon uchun ikkiga bo'ldik, kod — kanondagidek. String(process.pid) — typescript-eslint'ning restrict-template-expressions qoidasi shablon ichida sonni to'g'ridan qo'yishga ruxsat bermaydi.
src/kesh.tsdagikeshgaYozendiwriteFileemas,atomikYozni chaqiradi.manba.tsda keshga yozish xatosi natijani buzmaydi:keshga yozib bo'lmadi (…)ogohlantirishi vamanba: "api".
Kanon testi: atomik yozishdan keyin papkada .tmp qolmagani tekshiriladi (readdir bilan, xuddi yuqoridagi jonli misoldagidek).
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Kesh/sozlama ba'zan "buzilgan JSON" | Yozish o'rtasida to'xtatildi | Atomik yozish |
EXDEV: cross-device link not permitted |
.tmp boshqa diskda |
.tmp — maqsad fayl yonida |
EPERM rename da (Windows) |
Fayl boshqa dasta/jarayonda ochiq | Qayta urinish yoki xatoni yumshoq qabul qilish |
EMFILE: too many open files |
Dastalar oqadi yoki parallellik cheksiz | await using, bo'laklab ishlash |
startsWith(asos) tekshiruvi o'tdi, lekin fayl tashqarida |
Ajratgich yo'q (kesh-eski) |
asos + path.sep |
.tmp fayllar to'planib qoldi |
Jarayon SIGKILL bilan o'ldi |
Vaqti-vaqti bilan tozalash (kesh tozala) |
9. Mashqlar
1-mashq (oson): to'g'ri yoki noto'g'ri
- Atomik yozishda vaqtinchalik fayl
os.tmpdir()da bo'lishi kerak. renamebir papka ichida atomik.writeFiletugashi — ma'lumot albatta diskda degani.path.join(asos, nom)yo'l traversal'dan himoya qiladi.- Kanon tajribasida atomik yozish 20 urinishda nechta buzilgan fayl qoldirdi?
Yechim
- noto'g'ri — boshqa disk bo'lsa
renameatomik emas (EXDEV). - to'g'ri — fayl tizimidagi bitta yozuv almashadi.
- noto'g'ri — sahifa keshida bo'lishi mumkin; kafolat —
fsync(flush: true). - noto'g'ri —
join..ni bajaradi va tekshirmaydi. - 0 — oddiy yozishda 17.
2-mashq (o'rta): ichidami
ichidami(asos, toliq) funksiyasini path.posix.relative bilan yozing: toliq asos ichida bo'lsa true. Qoida: relative natijasi bo'sh satr (asos ning o'zi) bo'lsa — false (fayl emas, papkaning o'zi); .. bilan boshlansa yoki mutlaq bo'lsa — false. Uchta qiyin holatni sinang: /srv/kesh/a.json, /srv/kesh-eski/a.json, /srv/kesh.
Yechim
import path from "node:path";
const p = path.posix;
function ichidami(asos, toliq) {
const nisbiy = p.relative(asos, toliq);
if (nisbiy === "") return false;
if (nisbiy === ".." || nisbiy.startsWith("../")) return false;
return !p.isAbsolute(nisbiy);
}
const asos = "/srv/kesh";
console.log(ichidami(asos, "/srv/kesh/a.json"));
console.log(ichidami(asos, "/srv/kesh-eski/a.json"));
console.log(ichidami(asos, "/srv/kesh"));
console.log(ichidami(asos, "/srv/kesh/../kesh/b.json"));Konsolda:
true
false
false
truekesh-eski uchun relative → ../kesh-eski/a.json — rad. Oxirgisi .. bo'lsa ham ichkariga qaytadi: relative yo'lni avval normallashtiradi, shuning uchun to'g'ri true. Nega startsWith("..") emas, "../"? Chunki ..hisobot.json kabi zararsiz nom ham .. bilan boshlanadi.
3-mashq (qiyin): arxiv ochuvchi tekshiruvi
Arxiv yozuvlari ro'yxati berilgan (nom va matn). xavfsizOch(papka, yozuvlar) funksiyasini yozing: har yozuvni papka ichiga atomik yozsin, lekin tashqariga chiqadigan nomni yozmasin va "o'tkazildi: <nom>" deb chiqarsin. Oxirida papkadagi fayllarni (ichma-ich) saralab chiqaring. atomikYoz va xavfsizYol dan foydalaning (path.join, path.resolve — joriy OS uchun; xavfsizYol da path.sep).
Yechim
import { randomBytes } from "node:crypto";
import {
mkdir, readdir, rename, rm, writeFile,
} from "node:fs/promises";
import path from "node:path";
async function atomikYoz(fayl, malumot) {
await mkdir(path.dirname(fayl), { recursive: true });
const tasodif = randomBytes(4).toString("hex");
const tmp = `${fayl}.${process.pid}.${tasodif}.tmp`;
try {
await writeFile(tmp, malumot);
await rename(tmp, fayl);
} catch (xato) {
await rm(tmp, { force: true });
throw xato;
}
}
function xavfsizYol(asos, nom) {
const toliq = path.resolve(asos, nom);
return toliq.startsWith(asos + path.sep) ? toliq : null;
}
async function xavfsizOch(papka, yozuvlar) {
const asos = path.resolve(papka);
for (const { nom, matn } of yozuvlar) {
const fayl = xavfsizYol(asos, nom);
if (fayl === null) {
console.log(`o'tkazildi: ${nom}`);
continue;
}
await atomikYoz(fayl, matn);
}
}
await rm("chiqish", { recursive: true, force: true });
await xavfsizOch("chiqish", [
{ nom: "kurslar.csv", matn: "USD,11903.15" },
{ nom: "2026/oktyabr.csv", matn: "EUR,13329.15" },
{ nom: "../../.bashrc", matn: "zararli" },
{ nom: "a/../../tashqari.txt", matn: "zararli" },
]);
const fayllar = await readdir("chiqish", { recursive: true });
console.log(fayllar.map((f) => f.split(path.sep).join("/")).sort());Konsolda:
o'tkazildi: ../../.bashrc
o'tkazildi: a/../../tashqari.txt
[ '2026', '2026/oktyabr.csv', 'kurslar.csv' ]To'rtinchi nom ayyorroq: a/ bilan boshlanadi, lekin ikki .. uni tashqariga olib chiqadi — resolve buni ham ushladi. Oxirgi qatordagi split(path.sep).join("/") — Windows'dagi 2026\oktyabr.csv ni hamma OS'da bir xil ko'rsatish uchun (path va os).
4-mashq: CLI qadami
«CLI qadami» bo'limidagi uch qadamni bajaring. Tekshirish: valyuta kurs USD --yangila (VALYUTA_YANGILA=1) ikki marta, keyin kesh papkasida .tmp fayl yo'qligini ko'ring. Commit qilmang.
Yechim
Kesh papkasi hozircha qayerda bo'lsa (vaqtinchalik — joriy papka), ls yoki valyuta kesh bilan qarang: faqat kurslar.json. .tmp qolsa — catch dagi rm ishlamagan yoki jarayon o'ldirilgan. npm test dagi "atomik yozish" testi ham shuni tekshiradi.
10. Real ishda
- Ma'lumotlar bazalari (PostgreSQL, SQLite) — WAL jurnali, fsync va atomik almashtirish ustiga qurilgan. Bu darsdagi g'oya ularning poydevori. ACID darsida yana uchratasiz.
- npm, VS Code, Git — konfiguratsiya va lock-fayllarni atomik yozadi (npm —
write-file-atomic). Gitindex.lock—"wx"dagi qulf faylning aynan o'zi. - Fayl yuklash serverlari — path traversal va zip slip — OWASP ro'yxatidagi klassik zaiflik; har yuklash endpoint'ida tekshiriladi.
- Intervyu savollari: "Faylni xavfsiz yangilashni qanday qilasiz?", "
EMFILEnima va qanday davolanadi?", "Path traversal'dan himoya qanday?".
Xulosa
- Oddiy yozish o'rtada to'xtasa — eski ma'lumot ham, yangisi ham yo'qoladi (kanon: 20 dan 17 buzilgan).
- Atomik yozish: o'sha papkadagi noyob
.tmp→rename(kanon: 0 buzilgan); xatoda.tmpo'chiriladi. - Elektr o'chishidan to'liq himoya —
fsync(flush: true,FileHandle.sync()); Windows'da ochiq fayl ustigarename→EPERM. EMFILE— ochiq fayllar chegarasi (Windows'da 8 189, Linux'da ko'pincha 1 024): dastalarni yoping, parallellikni cheklang.- Path traversal va zip slip:
path.resolve+startsWith(asos + path.sep)(yokipath.relative), mutlaq yo'l va symlink'ni unutmang.
Keyingi dars: path va os: kross-platforma kod — join va resolve farqi, OS papkalari va valyuta keshining haqiqiy joyi; holat 18 ni commit qilamiz.
Manbalar
- Node.js 24 hujjati — fs.rename, writeFile
flush, filehandle.sync - POSIX — rename() (atomiklik kafolati)
- OWASP — Path Traversal
- Snyk — Zip Slip Vulnerability (2018): https://security.snyk.io/research/zip-slip-vulnerability
- graceful-fs (npm: 4.2.11), write-file-atomic (npm: 8.0.0) — 2026-10-23 holatiga
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!