Mundarija (28)
- Bu darsda
- 1. Nega bu kerak?
- 2. Haqiqiy tur: sehrli baytlar
- 2.1 Qo'lda tekshirish
- 2.2 file-type kutubxonasi
- 2.3 Sehrli baytlarning chegarasi: polyglot
- 2.4 Ruxsat ro'yxati
- 2.5 Matn fayllari: imzosi yo'q
- 3. Fayl nomi: faqat ko'rsatish uchun
- 3.1 Path traversal: Multer sozlamasi
- 3.2 Nomni tozalash
- 3.3 Saqlash kaliti — tasodifiy
- 4. SVG va HTML: haqiqiy brauzer o'lchovi
- 4.1 Kanon tajribasi
- 4.2 Himoya qatlamlari
- 5. Zip bomba
- 6. Antivirus: ClamAV
- 7. Hajm va son limitlari
- 8. Hujumchi nigohi
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): WebP imzosi
- 3-mashq (qiyin): turniAniqla ning toza nusxasi
- 11. Loyiha qadami: xavfsizlik.ts moduli
- 12. Real ishda
- Xulosa
- Manbalar
Fayl yuklash xavfsizligi: sehrli baytlar, nom va SVG xavfi
Qisqacha: Yuklangan faylning kengaytmasi ham,
Content-Typei ham — mijoz yozgan matn, ikkalasi ham yolg'on bo'lishi mumkin. Haqiqiy turni faylning birinchi baytlaridan (sehrli baytlar,file-typekutubxonasi) aniqlang va faqat ruxsat ro'yxatidagi turlarni oling. Faylni foydalanuvchi bergan nom bilan emas, tasodifiy kalit bilan va veb-ildizdan tashqarida saqlang. SVG va HTML'ni qabul qilmang: kanon haqiqiy brauzerda SVG ichidagi skript ishlashini ko'rsatdi.
Bu darsda
- Faylning haqiqiy turini sehrli baytlardan qo'lda va
file-typebilan aniqlaysiz. - Fayl nomini faqat ko'rsatish uchun tozalaysiz, saqlash uchun esa tasodifiy kalit berasiz.
- SVG va HTML nega xavfli ekanini haqiqiy brauzer o'lchovi bilan tushuntirasiz.
- Zip bomba va antivirus (ClamAV) qayerda kerakligini bilasiz.
- Loyiha uchun
xavfsizlik.tsmodulini va uning 6 ta testini yozasiz.
Oldin bilishingiz kerak: Fayl yuklash: multipart va Multer, Xavfsiz fayl operatsiyalari (path traversal, zip slip), zlib: siqish (sehrli baytlar, zip bomba), Static fayl server (nosniff).
Oldingi darsda Multer'ning fileFilter i exe faylni image/png deb o'tkazib yubordi. Bugun shu teshikni va yana beshtasini yopamiz.
1. Nega bu kerak?
Foydalanuvchi faylini qabul qilish — begona odamga serveringizga narsa qo'yishga ruxsat berish. Hujumchi bu imkoniyatdan bir nechta yo'l bilan foydalanadi:
| Hujum | Natijasi |
|---|---|
exe'ni rasm.png deb yuborish |
boshqa foydalanuvchi "rasm"ni yuklab, dasturni ishga tushiradi |
../../ li nom |
fayl kerakli papkadan tashqariga yoziladi |
| skriptli SVG yoki HTML | sizning domeningizda begona JavaScript ishlaydi (XSS) |
| 1 MB lik arxiv, ichida 1 GB | server ochayotganda disk yoki xotira tugaydi |
| zararli dastur (virus) | boshqa foydalanuvchilarga tarqaladi |
O'xshatish: aeroportdagi bagaj. Yo'lovchi chamadonga "kiyim" deb yozib qo'ygan — bu yorliq. Xavfsizlik xizmati yorliqqa emas, rentgenga qaraydi. Sehrli baytlar — faylning rentgeni.
Bu darsda har hujumni kanon tajribasida ko'ramiz, keyin himoyasini yozamiz va test bilan tekshiramiz.
2. Haqiqiy tur: sehrli baytlar
2.1 Qo'lda tekshirish
zlib: siqish darsida gzip doim 1f 8b bilan boshlanishini ko'rgansiz — bu sehrli baytlar (magic bytes). Har format o'z imzosi bilan boshlanadi. Kengaytmani o'zgartirish oson, birinchi baytlarni esa format buzilmasdan o'zgartirib bo'lmaydi.
const IMZOLAR = [
["image/png", [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]],
["image/jpeg", [0xff, 0xd8, 0xff]],
["application/pdf", [0x25, 0x50, 0x44, 0x46, 0x2d]], // %PDF-
["image/gif", [0x47, 0x49, 0x46, 0x38]], // GIF8
["application/zip", [0x50, 0x4b, 0x03, 0x04]], // PK
["application/x-msdownload", [0x4d, 0x5a]], // MZ
];
function turniTop(bosh) {
for (const [mime, imzo] of IMZOLAR) {
if (imzo.every((bayt, i) => bosh[i] === bayt)) return mime;
}
return undefined;
}
const baytlar = (matn) => new TextEncoder().encode(matn);
const png = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 0, 0]);
console.log(turniTop(png));
console.log(turniTop(baytlar("MZ bu aslida dastur")));
console.log(turniTop(baytlar("%PDF-1.7 ...")));
console.log(turniTop(baytlar("<svg><script></script></svg>")));Konsolda:
image/png
application/x-msdownload
application/pdf
undefined- PNG imzosi 8 bayt:
89 50 4E 47(.PNG) va qator oxiri belgilari. Kodda o'n oltilik (0x89),pngmassivida o'nlik (137) — qiymati bir xil. MZ— Windows dasturlarining boshi (1980-yillardagi dasturchi Mark Zbikowski ismining bosh harflari).- SVG — matn, uning sehrli baytlari yo'q. Shuning uchun
undefined.
2.2 file-type kutubxonasi
Yuzlab formatning imzosini qo'lda yozish — xato manbai. Ba'zi formatlarda imzo boshida emas (WebP: RIFF + 4 bayt hajm + WEBP), ba'zilarini farqlash uchun ichkariroq o'qish kerak (zip, docx, xlsx — hammasi PK bilan boshlanadi). file-type kutubxonasi buni biladi.
npm i file-type@22.2.0import { fileTypeFromBuffer } from "file-type";
console.log(await fileTypeFromBuffer(boshi));
// { ext: 'png', mime: 'image/png' } yoki undefinedfile-type 22 — faqat ESM paket (ES Modules Node'da). Kanon beshta "rasm"ni tekshirdi:
rasm.png (aslida exe — MZ) → file-type: {"ext":"exe","mime":"application/x-msdownload"}
rasm.png (aslida zip) → file-type: {"ext":"zip","mime":"application/zip"}
rasm.gif (GIF + JS polyglot) → file-type: {"ext":"gif","mime":"image/gif"}
rasm.svg → file-type: undefined
sahifa.html → file-type: undefinedIkkinchi va uchinchi qatorga e'tibor bering.
2.3 Sehrli baytlarning chegarasi: polyglot
Uchinchi fayl — polyglot (bir vaqtda ikki formatda to'g'ri fayl). Uning boshi GIF89a, davomi esa JavaScript izohi ichiga yashirilgan kod: GIF89a/*…*/=alert(1)//. file-type uni haqiqiy GIF deb tanidi — boshi rostdan GIF.
Sehrli baytlar faqat boshini tekshiradi. Fayl ichida nima borligini ular aytmaydi. Polyglot'ga qarshi ikki yo'l bor:
- Qayta kodlash. Rasmni ochib, piksellardan yangi fayl yasash. Yashirin baytlar yangi faylga o'tmaydi. Buni Rasmlarni qayta ishlash: sharp darsida qilamiz.
- Alohida domen. Foydalanuvchi fayllarini asosiy saytdan boshqa domendan berish. Google shunday qiladi: yuklangan fayllar
googleusercontent.comdan ochiladi. Skript ishlasa ham, u asosiy saytning cookie'larini ko'rmaydi.
2.4 Ruxsat ro'yxati
Qoida: "qaysi turlar taqiqlangan" emas, "qaysi turlar ruxsat etilgan" ro'yxatini tuting (Qo'lda routing darsidagi oq ro'yxat g'oyasi). Taqiqlar ro'yxati doim nimanidir unutadi. vazifalar ilovasi uchun kanon beshta turni tanladi:
| MIME | Kengaytma | Nega kerak |
|---|---|---|
image/png |
png |
skrinshot |
image/jpeg |
jpg |
telefon fotosi |
image/webp |
webp |
zamonaviy rasm |
application/pdf |
pdf |
hujjat, chek |
text/plain |
txt |
eslatma |
SVG va HTML ro'yxatda yo'q — sababi «SVG va HTML: haqiqiy brauzer o'lchovi» bo'limida. Kengaytma ham ro'yxatdan olinadi, mijozning rasm.PNG.exe idan emas.
2.5 Matn fayllari: imzosi yo'q
text/plain ning sehrli baytlari yo'q — file-type undefined qaytaradi. Kanon modul bunday faylni uch shart bilan qabul qiladi:
- Mijoz
text/plaindegan (boshqa noma'lum tur — rad). - Ichida nol bayt (NUL) yo'q va baytlar to'g'ri UTF-8.
- Matn
<bilan boshlanmaydi (aks holda u HTML yoki SVG bo'lishi mumkin).
To'g'ri UTF-8 ekanini TextDecoder ning fatal: true rejimi tekshiradi (Buffer va binar ma'lumot). Bir nozik joy bor: modul faylning faqat boshini (4 100 bayt) ko'radi. Kesilgan joyda ko'p baytli harf ikkiga bo'linishi mumkin:
const UTF8 = (opts) => new TextDecoder("utf-8", opts);
const sinash = (baytlar, stream) => {
try {
UTF8({ fatal: true }).decode(baytlar, { stream });
return "matn";
} catch (e) {
return e.name;
}
};
const enc = new TextEncoder();
const uzun = enc.encode("ў".repeat(2051)); // har harf 2 bayt
const bosh = uzun.subarray(0, 4100);
const chala = uzun.subarray(0, 4101); // oxirgi harfning yarmi
console.log(uzun.length);
console.log(sinash(bosh, false), sinash(chala, false));
console.log(sinash(chala, true));
console.log(sinash(new Uint8Array([0xff, 0xfe, 0x41]), false));Konsolda:
4102
matn TypeError
matn
TypeError- 4 100 bayt — butun harflar, xato yo'q.
- 4 101 bayt — oxirgi
ўning birinchi bayti qoldi. Oddiy rejimda buTypeError— yaxshi matn rad etilardi. stream: true— "davomi keladi" degani: chala oxir xato emas. Kanon modul aynan shuni qiladi.ff fe 41— UTF-8 da hech qachon bo'lmaydigan baytlar. Bu haqiqiy xato.
Chrome'da xato xabari Node'dagidan boshqacha, shuning uchun faqat nomini (e.name) chiqardik — ma'nosi bir xil.
Tekshirib ko'ring: Hujumchi HTML faylni
text/plaindeb yubordi, lekin boshiga 10 ta bo'sh qator qo'ydi. Modul uni ushlaydimi?
Javob
Ha. Modulning tekshiruvi ^?\s*< — boshidagi BOM va istalgancha bo'shliq (bo'sh qatorlar ham \s) dan keyin < bo'lsa, rad. Kanon testida aynan BOM + ikki bo'shliq + <html> sinalgan — belgilash-tili sababi bilan rad etildi.
3. Fayl nomi: faqat ko'rsatish uchun
3.1 Path traversal: Multer sozlamasi
Xavfsiz fayl operatsiyalari darsida ../ bilan papkadan chiqishni ko'rgansiz. Fayl yuklashda nom mijozdan keladi. Kanon "yomon" serverni yozdi: Multer filename sifatida originalname ni ishlatadi, fayllar public/yuklash ga tushadi. Hujumchi nomi ../../chiqdi.txt bo'lgan fayl yubordi — preservePath sozlamasining ikki qiymati bilan:
preservePath=false: 200 {"originalname":"chiqdi.txt","path":"<tmp>\\public\\yuklash\\chiqdi.txt"}
preservePath=true: 200 {"originalname":"../../chiqdi.txt","path":"<tmp>\\chiqdi.txt"}
public/ dan tashqarida fayl paydo bo'ldimi: true | public/ ichida: falseSukutda (false) Multer ichidagi busboy nomdan yo'l qismlarini olib tashladi — fayl joyida. preservePath: true bilan esa fayl public/yuklash dan ikki pog'ona yuqoriga yozildi. Bu sozlama "papkalar tuzilmasini saqlab yuklash" uchun bor. Uni yoqqan dasturchi bilmasdan eshikni ochadi.
Xulosa ikki qavat: sozlamaga tayanmang va mijoz nomi bilan umuman saqlamang.
3.2 Nomni tozalash
Asl nom baribir kerak: foydalanuvchi ro'yxatda chek (1).pdf ni ko'rishi va yuklab olganda xuddi shu nomni olishi kerak. Shuning uchun nom ma'lumot sifatida saqlanadi (bazada, keyin Fayl metama'lumoti darsida) va ekranga chiqishdan oldin tozalanadi.
Nomda nimalar xavfli:
- yo'l qismlari (
/,\) va..; - boshqaruv belgilari (
\r\n— HTTP sarlavhasini buzadi); - Windows taqiqlagan belgilar (
< > : " | ? *) va zaxira nomlar (CON,NUL,COM1); - ko'rinmas va yo'nalish belgilari: U+202E (RLO) matnni teskari ko'rsatadi —
hisobot+ RLO +fdp.exeekrandahisobotexe.pdfbo'lib ko'rinadi; - juda uzun nom.
Kanon funksiyasining JavaScript nusxasi (brauzerda ham ishlaydi):
const WINDOWS_ZAXIRA = /^(con|prn|aux|nul|com\d|lpt\d)$/i;
const kp = (a, b) =>
`${String.fromCodePoint(a)}-${String.fromCodePoint(b)}`;
const YOMON_BELGI = new RegExp(
String.raw`[\x00-\x1F\x7F<>:"/\\|?*` +
kp(0x200b, 0x200f) + kp(0x202a, 0x202e) + kp(0x2066, 0x2069) +
"]",
"g",
);
function faylNomiTozala(nom, maks = 120) {
const oxirgi = nom.normalize("NFC").split(/[/\\]/).at(-1) ?? "";
let toza = oxirgi
.replace(YOMON_BELGI, "_")
.replace(/^[.\s]+|[.\s]+$/g, "");
const nuqta = toza.lastIndexOf(".");
const asos = nuqta > 0 ? toza.slice(0, nuqta) : toza;
const kengaytma = nuqta > 0 ? toza.slice(nuqta) : "";
if (WINDOWS_ZAXIRA.test(asos)) toza = `_${toza}`;
if (Array.from(toza).length > maks) {
const k = Array.from(kengaytma).slice(0, 16).join("");
toza = Array.from(asos)
.slice(0, maks - Array.from(k).length)
.join("") + k;
}
return toza === "" || toza === "_" ? "fayl" : toza;
}
const rlo = String.fromCodePoint(0x202e);
console.log(faylNomiTozala("../../etc/passwd"));
console.log(faylNomiTozala("..\\..\\Windows\\win.ini"));
console.log(faylNomiTozala("CON.txt"));
console.log(faylNomiTozala(`hisobot${rlo}fdp.exe`));
console.log(faylNomiTozala("a\r\nSet-Cookie: x.txt"));
console.log(faylNomiTozala("...."));
console.log(faylNomiTozala("chek (1).pdf"));
const uzunNom = faylNomiTozala("я".repeat(300) + ".docx");
console.log(Array.from(uzunNom).length);Konsolda:
passwd
win.ini
_CON.txt
hisobot_fdp.exe
a__Set-Cookie_ x.txt
fayl
chek (1).pdf
120Qadamlar:
normalize("NFC")— bir harfning ikki xil yozilishini bittaga keltiradi (íbitta belgi yokii+ urg'u belgisi; macOS ikkinchisini yuboradi).split(/[/\\]/).at(-1)— oxirgi/yoki\dan keyingi qism: yo'l tashlanadi.- Xavfli belgilar
_ga almashadi. Belgilar kod raqami bilan yozilgan — manba kodida ko'rinmas belgi bo'lmasin. - Boshidagi va oxiridagi nuqta va bo'shliqlar olinadi (
....→ bo'sh →fayl). - Windows zaxira nomiga
_qo'shiladi. - Uzunlik — 120 ta kod nuqtasi (
Array.from), bayt emas; kengaytma saqlanadi.
Tozalangan nom ham diskka yozish uchun emas. U faqat ekranda va yuklab berishda (Content-Disposition, Fayl yuklab berish darsida) ishlatiladi.
3.3 Saqlash kaliti — tasodifiy
Fayl omborda server o'ylab topgan nom bilan yotadi:
function saqlashKaliti(egasiId, kengaytma, prefiks = "vazifalar") {
if (!/^[a-z0-9]{1,8}$/.test(kengaytma)) {
throw new TypeError(`noto'g'ri kengaytma: ${kengaytma}`);
}
return `${prefiks}/${egasiId}/${crypto.randomUUID()}.${kengaytma}`;
}
const a = saqlashKaliti(7, "png");
console.log(/^vazifalar\/7\/[0-9a-f-]{36}\.png$/.test(a));
console.log(a === saqlashKaliti(7, "png"));
try {
saqlashKaliti(7, "../x");
} catch (e) {
console.log(e.name, e.message);
}Konsolda:
true
false
TypeError noto'g'ri kengaytma: ../xKalit — vazifalar/7/ + UUID + kengaytma. Kalit tasodifiy bo'lgani uchun natijani to'g'ridan chiqarmadik: har ishga tushishda boshqa. Kengaytma faqat ruxsat ro'yxatidan keladi, lekin funksiya baribir tekshiradi — kim chaqirsa ham, kalitga ../ tushmaydi. Bu chuqur himoya (defense in depth): bir qatlam xato qilsa, keyingisi ushlaydi.
vazifalar/7/ — egasining ID si. U kalitni tartibli qiladi, lekin huquqni tekshirmaydi. Faylni kim ko'ra olishi — bazadagi egalik yozuvi bilan hal qilinadi (28-qismda RBAC va resurs egaligi).
Tekshirib ko'ring: Nega kengaytmani
originalnamedan olish xato, hatto tur sehrli baytlar bilan tekshirilgan bo'lsa ham?
Javob
Haqiqiy PNG faylni rasm.html nomi bilan yuborish mumkin. Fayl ….html bo'lib saqlansa, ba'zi serverlar uni kengaytmaga qarab text/html bilan beradi. Kengaytma tekshirilgan turdan (image/png → png) olinsa, nom va mazmun doim mos.
4. SVG va HTML: haqiqiy brauzer o'lchovi
4.1 Kanon tajribasi
SVG — rasm formati, lekin ichida <script> bo'lishi mumkin. Bu nazariya emasmi? Kanon Express serverida bitta skriptli SVG ni olti xil usulda berdi va haqiqiy Chromium (Playwright 1.62, headless) bilan ochdi. Skript ishlasa, sahifa sarlavhasini o'zgartiradi:
/inline.svg → title = "XSS ISHLADI: 127.0.0.1"
/attachment.svg → yuklab olindi (rasm.svg) — sahifada ochilmadi
/csp.svg → title = ""
/img-ichida.html → title = "sahifa"
/matn-nosniffsiz → title = "XSS ISHLADI (sniff)"
/matn-nosniff → title = ""| Qanday berildi | Skript |
|---|---|
image/svg+xml, to'g'ridan ochildi |
ishladi |
Content-Disposition: attachment |
ishlamadi (yuklab olindi) |
Content-Security-Policy: sandbox |
ishlamadi |
<img src="…svg"> ichida |
ishlamadi |
HTML, Content-Type siz |
ishladi (brauzer taxmin qildi) |
text/plain + nosniff |
ishlamadi |
Birinchi qator — eng xavflisi. Hujumchi "avatar.svg" yuklaydi va havolasini boshqa foydalanuvchiga yuboradi. Havola ochilganda skript sizning domeningizda ishlaydi: sahifadagi ma'lumotni o'qiydi va foydalanuvchi nomidan so'rov yuboradi. Bu saqlangan XSS (stored XSS).
<img> ichida esa SVG faqat rasm sifatida chiziladi — brauzer skriptni o'chiradi. Lekin hujumchi havolani to'g'ridan ochtiradi, <img> orqali emas.
Beshinchi qator — MIME sniffing: Content-Type bo'lmasa, Chromium tanadagi <html> ni ko'rib, HTML deb taxmin qildi.
4.2 Himoya qatlamlari
- Qabul qilmang. Kanon ruxsat ro'yxatida SVG va HTML yo'q — eng ishonchli yo'l.
- Veb-ildizdan tashqarida saqlang. Yuklangan fayllar
express.staticpapkasiga (Mashhur middleware'lar) tushsa, server ularni kengaytmaga qarabtext/htmlyokiimage/svg+xmlbilan beradi. Kanon tajribasidagipublic/yuklashaynan shu xato. Fayllar omborda, ular faqat sizning marshrutingiz orqali beriladi. - To'g'ri sarlavhalar bilan bering: tekshirilgan
Content-Type,X-Content-Type-Options: nosniff, ko'rsatish shart bo'lmasaContent-Disposition: attachment. - Alohida domen va CSP sandbox — SVG baribir kerak bo'lsa (masalan, dizaynerlar uchun ilova). Yoki SVG ni serverda PNG ga aylantiring.
flowchart TD
F[Yuklangan fayl] --> H{Hajm limiti}
H -->|oshdi| R1[413]
H --> S{Sehrli baytlar}
S -->|ro'yxatda yo'q| R2[415]
S -->|matn| M{UTF-8 va boshi '<' emas}
M -->|yo'q| R2
S -->|png, jpg, webp, pdf| K[Tasodifiy kalit]
M -->|ha| K
K --> O[(Private ombor)]
O --> B[Marshrut: nosniff, attachment]Diagrammaga qarang: fayl har bosqichda rad etilishi mumkin, va faqat hamma tekshiruvdan o'tgani tasodifiy kalit bilan omborga tushadi. Foydalanuvchiga u faqat marshrut orqali, to'g'ri sarlavhalar bilan qaytadi.
5. Zip bomba
zlib: siqish darsida gzip bombasini ko'rgansiz. Zip arxiv ham xuddi shunday. Kanon archiver 8 bilan arxiv yasadi: ichida to'rtta 256 MB lik nol fayl.
arxiv: 1019.7 KB, ichida 4 × 256 MB = 1 GB nol
cheklov 100 MB (e'lon): RAD (e'lon qilingan): 1-yozuvda jami 256.0 MB > 100.0 MB; nisbat 1029:1
faqat oqim cheklovi 100 MB: RAD (haqiqiy oqim): 100.0 MB o'qilganda to'xtatildi
cheklovsiz: hammasi ochildi: 4 yozuv, 1024.0 MBBir megabaytdan kichik fayl — bir gigabayt. Siqish nisbati 1029:1. Foydalanuvchi "hisobotlar.zip" ni yuklasa va server uni ochsa (masalan, ichidagi rasmlarni ko'rsatish uchun), 100 ta shunday arxiv 100 GB.
Kanon yauzl kutubxonasi bilan ikki himoyani sinadi:
- E'lon qilingan hajm. Zip arxivning mundarijasida har faylning ochilgandagi hajmi yozilgan. Jami limitdan oshsa — arxivni umuman ochmaymiz. Tez, lekin bu raqamni hujumchi yozgan.
- Haqiqiy oqim hisoblagichi. Ochilayotgan baytlarni sanaymiz va limitda to'xtatamiz. Hujumchi raqamni yolg'on yozsa ham ishlaydi.
Ikkalasini birga qo'ying. Va ichidagi fayl nomlarini zip slip uchun tekshirishni unutmang. vazifalar ilovasi arxivlarni qabul qilmaydi (application/zip ruxsat ro'yxatida yo'q) — eng oddiy himoya.
Maslahat: archiver 8 faqat ESM. Internetdagi ko'p misollarda
archiver("zip")fabrikasi bor — 8-versiyada u yo'q,new ZipArchive()ishlatiladi (kanon topilmasi).
6. Antivirus: ClamAV
Sehrli baytlar "bu PDF" deydi, lekin "bu PDF ichida zararli kod bor" demaydi. Fayllarni boshqa foydalanuvchilar yuklab olsa (hujjat almashish, korporativ tizimlar), ularni antivirus bilan tekshirish talab qilinadi.
ClamAV — ochiq kodli antivirus. Serverda uning xizmati clamd ishlaydi. Ilova unga faylni INSTREAM buyrug'i bilan bo'laklab yuboradi, clamd esa "toza" yoki topilgan virus nomini qaytaradi. Juda katta fayl (StreamMaxLength dan oshsa) rad etiladi.
Diqqat: ClamAV kanonda sinalmagan: uning imzo bazasini internetdan yuklash va alohida xizmat kerak edi. Yuqoridagi tavsif — rasmiy qo'llanma bo'yicha (2026-10-28 da o'qildi). Kodini va natijasini o'ylab topmaymiz.
Amalda: tekshiruv sekin (soniyalar), shuning uchun u navbatda bajariladi (BullMQ asoslari), fayl esa tekshiruv tugaguncha "kutilmoqda" holatida turadi.
7. Hajm va son limitlari
Oldingi darsdagi Multer limits — birinchi chiziq. Yana ikki joyda limit kerak:
- Foydalanuvchi bo'yicha. Bitta vazifaga 10 tadan ortiq ilova emas, bitta foydalanuvchiga jami N MB. Bu bazada tekshiriladi (Fayl metama'lumoti darsida kanon limit 10 ni tranzaksiya bilan himoyalaydi).
- Vaqt bo'yicha. Bir daqiqada nechta yuklash — Rate limiting.
Limitsiz har bir himoya baribir yorilib ketadi: 10 000 ta to'g'ri PNG ham diskni to'ldiradi.
8. Hujumchi nigohi
Endi butun darsni hujumchi ko'zi bilan jamlaymiz: har hujum, uni nima to'xtatadi va kanon buni qanday tekshirgan.
| Hujum | Himoya | Kanon tekshiruvi |
|---|---|---|
exe → image/png |
sehrli baytlar + ruxsat ro'yxati | test: ruxsat-yoq |
HTML → text/plain |
UTF-8, < tekshiruvi |
test: belgilash-tili |
../../ nom |
nom faqat ko'rsatish uchun, kalit UUID | test: passwd, win.ini |
RLO, \r\n nomda |
faylNomiTozala |
test: hisobot_fdp.exe |
| skriptli SVG | ro'yxatda yo'q, nosniff, attachment |
brauzer: 6 holat |
| zip bomba | arxiv qabul qilinmaydi; e'lon + oqim limiti | 1029:1 tajriba |
| GIF+JS polyglot | qayta kodlash, alohida domen | file-type → image/gif |
Oxirgi qatorga e'tibor bering: polyglot'ni sehrli baytlar ushlamaydi. Kanon modul GIF ni ruxsat ro'yxatiga qo'shmagan, PNG/JPEG/WebP esa sharp bilan qayta kodlanadi (loyihaga ulanganda — navbat orqali). Himoya — bir nechta qatlam, bittasi emas.
9. Ko'p uchraydigan xatolar
| Xato | Nega xavfli | To'g'risi |
|---|---|---|
file.mimetype ga ishonish |
mijoz yozgan | file-type + ruxsat ro'yxati |
kengaytmani originalname dan olish |
rasm.html |
kengaytma aniqlangan turdan |
originalname bilan saqlash |
to'qnashuv, traversal | randomUUID() kalit |
preservePath: true |
fayl papkadan chiqadi | sukut false + o'z kalitingiz |
yuklashlarni public/ ga qo'yish |
SVG/HTML skripti ishlaydi | ombor + marshrut |
javobda nosniff yo'q |
brauzer turni taxmin qiladi | X-Content-Type-Options: nosniff |
TextDecoder fatal siz |
noto'g'ri baytlar � bo'lib o'tadi |
{ fatal: true } |
taqiqlar ro'yxati (.exe yo'q) |
.scr, .hta ni unutasiz |
ruxsat ro'yxati |
10. Mashqlar
Mashqlar kurs/mashqlar/29/02-xavfsizlik/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
fileFilterdafile.mimetype === "image/png"tekshiruvi exe faylni to'xtatadi.file-typeGIF+JS polyglot faylniimage/gifdeb taniydi.<img src="avatar.svg">ichida SVG skripti ishlaydi.faylNomiTozaladan chiqqan nom bilan faylni diskka yozsa bo'ladi.- Zip arxivning e'lon qilingan hajmiga to'liq ishonsa bo'ladi.
Yechim
- Noto'g'ri —
mimetypemijoz matni; kanon tajribasida exe o'tdi. - To'g'ri — boshi rostdan GIF. Shuning uchun qayta kodlash yoki alohida domen kerak.
- Noto'g'ri — kanon brauzer o'lchovi:
<img>ichida ishlamadi. Lekin to'g'ridan ochilganda ishladi. - Noto'g'ri — u faqat ko'rsatish uchun. Saqlash — tasodifiy kalit bilan.
- Noto'g'ri — raqamni hujumchi yozadi; haqiqiy oqim hisoblagichi ham kerak.
2-mashq (o'rta): WebP imzosi
WebP fayl RIFF (4 bayt), keyin 4 baytli hajm (istalgan qiymat), keyin WEBP bilan boshlanadi. turniTop ga o'xshash webpmi(bosh) funksiyasini yozing: 0–3 baytlar RIFF va 8–11 baytlar WEBP bo'lsa true. Uch holatda sinang: haqiqiy WebP boshi, RIFF....WAVE (audio fayl) va 6 baytli qisqa massiv. Ishora: satrni baytlarga TextEncoder aylantiradi, kesish — subarray.
Yechim
const enc = new TextEncoder();
function mosmi(bosh, joy, matn) {
const kutilgan = enc.encode(matn);
const qism = bosh.subarray(joy, joy + kutilgan.length);
if (qism.length !== kutilgan.length) return false;
return kutilgan.every((bayt, i) => qism[i] === bayt);
}
function webpmi(bosh) {
return mosmi(bosh, 0, "RIFF") && mosmi(bosh, 8, "WEBP");
}
const hajm = [0x24, 0x00, 0x00, 0x00];
const webp = new Uint8Array([
...enc.encode("RIFF"), ...hajm, ...enc.encode("WEBPVP8 "),
]);
const wav = new Uint8Array([
...enc.encode("RIFF"), ...hajm, ...enc.encode("WAVEfmt "),
]);
console.log(webpmi(webp));
console.log(webpmi(wav));
console.log(webpmi(enc.encode("RIFF..")));Konsolda:
true
false
falseqism.length tekshiruvi qisqa massivda muhim: subarray chegaradan chiqsa xato bermaydi, qisqa natija qaytaradi. Usiz every bo'sh massivda true qaytarishi mumkin edi. WAV ham RIFF bilan boshlanadi — shuning uchun faqat birinchi 4 baytga qarash yetmaydi. file-type bu kabi holatlarni yuzlab formatda biladi.
3-mashq (qiyin): turniAniqla ning toza nusxasi
Kanon turniAniqla(bosh, elon) qoidasini file-type siz yozing: ikkilik tur darsdagi turniTop bilan aniqlanadi (IMZOLAR dan faqat PNG, JPEG, PDF, exe, zip). Qaytsin: { ok: true, mime, kengaytma } yoki { ok: false, sabab } (ruxsat-yoq, noma'lum-tur, matn-emas, belgilash-tili). Ruxsat — PNG, JPEG, PDF va matn. Matn qoidalari «Matn fayllari: imzosi yo'q» bo'limidagidek. Olti holatni sinang: PNG, exe (image/png deb), oddiy matn, NUL li matn, BOM + <html>, application/json deb yuborilgan matn.
Yechim
const IMZOLAR = [
["image/png", [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]],
["image/jpeg", [0xff, 0xd8, 0xff]],
["application/pdf", [0x25, 0x50, 0x44, 0x46, 0x2d]],
["application/zip", [0x50, 0x4b, 0x03, 0x04]],
["application/x-msdownload", [0x4d, 0x5a]],
];
const RUXSAT = {
"image/png": "png",
"image/jpeg": "jpg",
"application/pdf": "pdf",
};
const BELGILASH = new RegExp(
`^${String.fromCodePoint(0xfeff)}?` + String.raw`\s*<`,
);
function turniTop(bosh) {
for (const [mime, imzo] of IMZOLAR) {
if (imzo.every((bayt, i) => bosh[i] === bayt)) return mime;
}
return undefined;
}
function turniAniqla(bosh, elon) {
const topildi = turniTop(bosh);
if (topildi) {
return topildi in RUXSAT
? { ok: true, mime: topildi, kengaytma: RUXSAT[topildi] }
: { ok: false, sabab: "ruxsat-yoq" };
}
const tur = elon.split(";")[0].trim().toLowerCase();
if (tur !== "text/plain") {
return { ok: false, sabab: "noma'lum-tur" };
}
if (bosh.includes(0)) return { ok: false, sabab: "matn-emas" };
let matn;
try {
matn = new TextDecoder("utf-8", { fatal: true }).decode(bosh);
} catch {
return { ok: false, sabab: "matn-emas" };
}
if (BELGILASH.test(matn)) {
return { ok: false, sabab: "belgilash-tili" };
}
return { ok: true, mime: "text/plain", kengaytma: "txt" };
}
const b = (s) => new TextEncoder().encode(s);
const png = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
const bom = String.fromCodePoint(0xfeff);
console.log(turniAniqla(png, "application/octet-stream"));
console.log(turniAniqla(b("MZ dastur"), "image/png"));
console.log(turniAniqla(b("Oʻzbekiston"), "text/plain"));
console.log(turniAniqla(b("a\u0000b"), "text/plain"));
console.log(turniAniqla(b(`${bom} <html>`), "text/plain"));
console.log(turniAniqla(b("{}"), "application/json"));Konsolda:
{ ok: true, mime: 'image/png', kengaytma: 'png' }
{ ok: false, sabab: 'ruxsat-yoq' }
{ ok: true, mime: 'text/plain', kengaytma: 'txt' }
{ ok: false, sabab: 'matn-emas' }
{ ok: false, sabab: 'belgilash-tili' }
{ ok: false, sabab: "noma'lum-tur" }PNG uchun mijoz application/octet-stream dedi — ahamiyati yo'q, baytlar hal qildi. topildi in RUXSAT — obyektda shunday kalit bormi (in va Object.hasOwn). Kanon modulda bu yerda Object.hasOwn — in prototip zanjiridagi toString kabi kalitlarni ham "bor" deydi, hasOwn esa faqat obyektning o'zini tekshiradi. Bu yechimda topildi faqat IMZOLAR dagi qiymat bo'lishi mumkin, shuning uchun farq yo'q.
11. Loyiha qadami: xavfsizlik.ts moduli
Kanon bu darsning bilimini alohida modulga yig'di: modullar/src/fayl/xavfsizlik.ts. U vazifalar-api dan mustaqil — keyingi darsdagi oqimli yuklash uni chaqiradi. Modul file-type 22.2.0 ga tayanadi (TypeScript 6.0.3, Node 24.21.0). Telefon uchun uzun qatorlar va izohlarni bo'ldik, kod ma'nosi kanondagidek:
// xavfsizlik.ts — yuklangan faylga ishonmaslik: turi magic bytes
// bo'yicha, nom faqat ko'rsatish uchun, saqlash kaliti tasodifiy.
// Kengaytma va Content-Type — mijoz yozgan matn, ularga qarab qaror
// qilinmaydi.
import { randomUUID } from "node:crypto";
import { fileTypeFromBuffer } from "file-type";
/** Ruxsat etilgan turlar: MIME → saqlashdagi kengaytma.
* SVG va HTML ataylab yo'q (ichida skript bo'lishi mumkin). */
export const RUXSAT_ETILGAN = {
"image/png": "png",
"image/jpeg": "jpg",
"image/webp": "webp",
"application/pdf": "pdf",
"text/plain": "txt",
} as const;
export type RuxsatTur = keyof typeof RUXSAT_ETILGAN;
/** Turni aniqlash uchun boshidan shuncha bayt yetadi
* (file-type ko'p formatni 4 100 baytda taniydi). */
export const BOSH_BAYT = 4100;
export type TurNatija =
| { ok: true; mime: RuxsatTur; kengaytma: string }
| {
ok: false;
sabab: "noma'lum-tur" | "ruxsat-yoq" | "matn-emas"
| "belgilash-tili";
topildi?: string;
};
const isRuxsat = (m: string): m is RuxsatTur =>
Object.hasOwn(RUXSAT_ETILGAN, m);
/** Faylning birinchi baytlari bo'yicha turi. `elon` — mijoz aytgan
* Content-Type (faqat matn fayli uchun ishora). Ikkilik format
* topilsa — o'shaniki hal qiladi. Topilmasa va mijoz `text/plain`
* desa — UTF-8 matnmi va `<` bilan boshlanmaydimi — tekshiriladi. */
export async function turniAniqla(
bosh: Uint8Array,
elon: string,
): Promise<TurNatija> {
const topildi = await fileTypeFromBuffer(bosh);
if (topildi) {
return isRuxsat(topildi.mime)
? {
ok: true,
mime: topildi.mime,
kengaytma: RUXSAT_ETILGAN[topildi.mime],
}
: { ok: false, sabab: "ruxsat-yoq", topildi: topildi.mime };
}
const tur = elon.split(";")[0]?.trim().toLowerCase();
if (tur !== "text/plain") {
return { ok: false, sabab: "noma'lum-tur" };
}
if (bosh.includes(0)) return { ok: false, sabab: "matn-emas" };
// bosh BOSH_BAYT chegarasida kesilgan bo'lsa, oxirgi UTF-8 belgi
// chala bo'lishi mumkin — `stream: true` uni xato demaydi
let matn: string;
try {
matn = new TextDecoder("utf-8", { fatal: true })
.decode(bosh, { stream: bosh.length >= BOSH_BAYT });
} catch {
return { ok: false, sabab: "matn-emas" };
}
if (BELGILASH_BOSHI.test(matn)) {
return { ok: false, sabab: "belgilash-tili" };
}
return { ok: true, mime: "text/plain", kengaytma: "txt" };
}
const WINDOWS_ZAXIRA = /^(con|prn|aux|nul|com\d|lpt\d)$/i;
// Boshqaruv belgilari, yo'l ajratgichlar, Windows taqiqlagan
// belgilar, nol kenglikdagi va bidi belgilari (U+202E RLO).
// Kod nuqtalari raqam bilan — manbada ko'rinmas belgi bo'lmasin.
const kp = (a: number, b: number) =>
`${String.fromCodePoint(a)}-${String.fromCodePoint(b)}`;
const YOMON_BELGI = new RegExp(
String.raw`[\x00-\x1F\x7F<>:"/\\|?*` +
kp(0x200b, 0x200f) + kp(0x202a, 0x202e) + kp(0x2066, 0x2069) +
"]",
"g",
);
const BELGILASH_BOSHI = new RegExp(
`^${String.fromCodePoint(0xfeff)}?` + String.raw`\s*<`,
);
/** Asl nomni FAQAT ko'rsatish va `Content-Disposition` uchun
* tozalaydi. Diskka/bucket'ga bu nom bilan YOZILMAYDI. */
export function faylNomiTozala(nom: string, maks = 120): string {
const oxirgi = nom.normalize("NFC").split(/[/\\]/).at(-1) ?? "";
let toza = oxirgi
.replace(YOMON_BELGI, "_")
.replace(/^[.\s]+|[.\s]+$/g, "");
const nuqta = toza.lastIndexOf(".");
const asos = nuqta > 0 ? toza.slice(0, nuqta) : toza;
const kengaytma = nuqta > 0 ? toza.slice(nuqta) : "";
if (WINDOWS_ZAXIRA.test(asos)) toza = `_${toza}`;
// kod nuqtalari (emoji klasterlari emas — fayl nomi uchun yetarli)
const belgilar = Array.from(toza);
if (belgilar.length > maks) {
const k = Array.from(kengaytma).slice(0, 16).join("");
toza = Array.from(asos)
.slice(0, maks - Array.from(k).length)
.join("") + k;
}
return toza === "" || toza === "_" ? "fayl" : toza;
}
/** Saqlash kaliti — foydalanuvchi bergan nomdan mustaqil:
* `vazifalar/<egasi>/<uuid>.<kengaytma>`. */
export function saqlashKaliti(
egasiId: number | string,
kengaytma: string,
prefiks = "vazifalar",
): string {
if (!/^[a-z0-9]{1,8}$/.test(kengaytma)) {
throw new TypeError(`noto'g'ri kengaytma: ${kengaytma}`);
}
return `${prefiks}/${String(egasiId)}/${randomUUID()}.${kengaytma}`;
}TypeScript'dagi farqlar JS nusxadan: as const bilan ruxsat ro'yxati turga aylanadi (RuxsatTur), isRuxsat — tur qo'riqchisi (type guard), natija — ok maydoni bo'yicha discriminated union.
Testlar — modullar/test/fayl-xavfsizlik.test.ts, oltita. Haqiqiy PNG, JPEG va WebP ni sharp o'zi yasaydi (4×4 piksel), exe — qo'lda yasalgan MZ + PE sarlavhali 512 bayt. Testdan parcha:
test(
"ruxsat etilmagan ikkilik tur — " +
"exe ni image/png deb yuborsa ham rad",
async () => {
const exe = new Uint8Array(512);
exe.set(matn("MZ"), 0);
exe.set([0x50, 0x45, 0, 0], 0x80); // PE sarlavhasi
new DataView(exe.buffer).setUint32(0x3c, 0x80, true);
const r = await turniAniqla(exe, "image/png");
assert.deepEqual(r, {
ok: false, sabab: "ruxsat-yoq",
topildi: "application/x-msdownload",
});
const gif = await turniAniqla(await rasm("gif"), "image/gif");
assert.deepEqual(gif, {
ok: false, sabab: "ruxsat-yoq", topildi: "image/gif",
});
});GIF haqiqiy rasm bo'lsa ham rad — u ruxsat ro'yxatida yo'q (polyglot xavfi). Testlarni biz ishga tushirdik (kanon nusxasi, Node 24.21.0):
node --test test/fayl-xavfsizlik.test.ts✔ magic bytes: haqiqiy PNG/JPEG/WebP/PDF taniladi, e'lon qilingan tur ahamiyatsiz (11.2252ms)
✔ ruxsat etilmagan ikkilik tur — exe ni image/png deb yuborsa ham rad (3.3297ms)
✔ matn fayli: UTF-8 o'zbekcha o'tadi; NUL, noto'g'ri UTF-8, SVG/HTML rad (1.6205ms)
✔ UTF-8 belgi chegarada kesilgan bo'lsa ham matn o'tadi (0.4337ms)
✔ faylNomiTozala: yo'l, boshqaruv, Windows zaxira nomlari, bidi, uzunlik (2.1842ms)
✔ saqlashKaliti: tasodifiy, egasi bo'yicha, kengaytma qat'iy (0.7851ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 207.0538Millisekundlar har ishga tushishda boshqacha. Butun modullar paketi kanonda npm run check dan o'tgan: tsc 0, eslint 0 (strictTypeChecked), testlar 82/82.
Commit xabari (modul uchun):
git commit -m "feat(fayl): xavfsizlik — sehrli baytlar, nom tozalash, kalit"vazifalar-api ga ulash 28-qism (auth) tugagach bo'ladi: saqlash kalitidagi egasiId — tizimga kirgan foydalanuvchi.
12. Real ishda
- OWASP File Upload Cheat Sheet — sohadagi standart tekshiruv ro'yxati: ruxsat ro'yxati, tur tekshiruvi, nom, hajm, saqlash joyi, antivirus. Bu darsdagi hamma narsa unda bor.
- Katta platformalar foydalanuvchi fayllarini alohida domendan beradi (Google —
googleusercontent.com, GitHub —githubusercontent.com). - Bug bounty (zaiflik topganga pul to'lanadigan dasturlar) hisobotlarida SVG va fayl yuklash orqali stored XSS — tez-tez uchraydigan topilma.
- Intervyuda: "Fayl turini qanday tekshirasiz?", "Nega SVG xavfli?", "Fayl nomini qanday saqlaysiz?"
Xulosa
- Kengaytma va
Content-Type— mijoz matni; haqiqiy tur — sehrli baytlardan (file-type), faqat ruxsat ro'yxati. - Sehrli baytlar polyglot'ni ushlamaydi — qayta kodlash yoki alohida domen.
- Matn fayli: UTF-8 (
fatal, chegaradastream: true), NUL yo'q,<bilan boshlanmaydi. - Nom — faqat ko'rsatish uchun va tozalangan; saqlash —
egasi/uuid.kengaytmakaliti bilan. - Skriptli SVG to'g'ridan ochilganda haqiqiy brauzerda ishladi; himoya — qabul qilmaslik,
nosniff, attachment, alohida domen. - Zip bomba 1029:1 — e'lon qilingan hajm va haqiqiy oqim hisoblagichi birga.
Keyingi dars: Katta fayllarni stream bilan yuklash — 200 MB faylni xotiraga yig'masdan omborga oqizamiz, turniAniqla ni oqim boshida chaqiramiz va uch usulni xotira o'lchovi bilan solishtiramiz.
Manbalar
- OWASP File Upload Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
- file-type: https://github.com/sindresorhus/file-type
- Multer
preservePath: https://github.com/expressjs/multer#multeropts TextDecoder(fatal,stream): https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decodeX-Content-Type-Options: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options- ClamAV
clamd(INSTREAM): https://docs.clamav.net/manual/Usage/Scanning.html — sinalmagan, 2026-10-28 da o'qildi - yauzl: https://github.com/thejoshwolfe/yauzl
- Kanon tajribasi (IlmHamroh, 2026-10-27): multer 2.4.0, file-type 22.2.0, archiver 8.0.0, yauzl 3.4.0, playwright-core 1.62.0 (Chromium headless shell), Node 24.21.0. Modul testlari — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!