Валидация файлов загрузки отличается от проверки обычных строковых полей. Помимо текстовых значений необходимо учитывать:
Библиотека Validator.js работает только со строками, поэтому при проверке файлов обычно валидируются:
originalnamemimetypeФактическая проверка бинарного содержимого выполняется отдельно.
npm install validator
Импорт в CommonJS:
const validator = require('validator');
Импорт в ES Modules:
import validator from 'validator';
В Node.js файл часто приходит через middleware:
Пример объекта:
{
fieldname: 'avatar',
originalname: 'photo.png',
encoding: '7bit',
mimetype: 'image/png',
size: 248932,
destination: 'uploads/',
filename: '8fd7c9.png',
path: 'uploads/8fd7c9.png'
}
Validator.js применяется к строковым свойствам:
validator.isMimeType(file.mimetype);
validator.isLength(file.originalname, { min: 1, max: 255 });
if (!validator.isLength(file.originalname, { min: 1, max: 255 })) {
throw new Error('Некорректное имя файла');
}
Некоторые символы могут использоваться для атак:
const forbidden = /[<>:"/\\|?*\x00-\x1F]/;
if (forbidden.test(file.originalname)) {
throw new Error('Имя файла содержит запрещённые символы');
}
const safeName = validator.whitelist(
file.originalname,
'a-zA-Z0-9._-'
);
if (safeName !== file.originalname) {
throw new Error('Недопустимые символы');
}
const sanitized = validator.escape(file.originalname);
Однако escape() предназначен прежде всего для
HTML-контекста:
avatar<script>.png
Результат:
avatar<script>.png
Для файловой системы обычно применяют регулярные выражения.
const path = require('path');
const ext = path.extname(file.originalname).toLowerCase();
const allowed = ['.jpg', '.jpeg', '.png', '.webp'];
if (!allowed.includes(ext)) {
throw new Error('Формат файла запрещён');
}
Опасный пример:
photo.jpg.exe
Проверка:
const parts = file.originalname.split('.');
if (parts.length > 2) {
throw new Error('Обнаружено двойное расширение');
}
Более гибкий вариант:
const dangerous = ['.exe', '.bat', '.cmd', '.sh'];
if (dangerous.some(d => file.originalname.endsWith(d))) {
throw new Error('Опасный тип файла');
}
if (!validator.isMimeType(file.mimetype)) {
throw new Error('Некорректный MIME-тип');
}
const allowedMime = [
'image/jpeg',
'image/png',
'image/webp'
];
if (!allowedMime.includes(file.mimetype)) {
throw new Error('Недопустимый MIME-тип');
}
Клиент может отправить:
image/png
для файла:
virus.exe
Поэтому MIME-тип нельзя считать надёжным источником.
Для безопасной проверки используют анализ сигнатур файла:
Validator.js не валидирует числа напрямую, поэтому размер преобразуется в строку либо проверяется обычным JavaScript.
const MAX_SIZE = 5 * 1024 * 1024;
if (file.size > MAX_SIZE) {
throw new Error('Файл слишком большой');
}
if (!validator.isInt(String(file.size), { min: 0, max: 5242880 })) {
throw new Error('Некорректный размер файла');
}
if (files.length > 10) {
throw new Error('Слишком много файлов');
}
if (files.length < 1) {
throw new Error('Файлы отсутствуют');
}
const imageExtensions = [
'.jpg',
'.jpeg',
'.png',
'.gif',
'.webp'
];
const imageMime = [
'image/jpeg',
'image/png',
'image/gif',
'image/webp'
];
if (!imageMime.includes(file.mimetype)) {
throw new Error('Недопустимое изображение');
}
SVG-файлы опасны возможностью внедрения Jav * aScript:
<script>alert(1)</script>
Поэтому SVG требует дополнительной очистки.
Проверка:
if (file.mimetype === 'image/svg+xml') {
throw new Error('SVG запрещён');
}
Либо использовать sanitizer.
if (ext !== '.pdf') {
throw new Error('Только PDF');
}
if (file.mimetype !== 'application/pdf') {
throw new Error('Некорректный PDF');
}
PDF начинается с:
%PDF
Пример проверки:
const fs = require('fs');
const buffer = fs.readFileSync(file.path);
const signature = buffer.toString('utf8', 0, 4);
if (signature !== '%PDF') {
throw new Error('Поддельный PDF');
}
Архивы особенно опасны:
const allowedArchives = [
'.zip',
'.rar',
'.7z'
];
Некоторые серверы запрещают:
const forbidden = ['.tar', '.gz'];
if (forbidden.includes(ext)) {
throw new Error('Архив запрещён');
}
../. ./. ./etc/passwd
if (file.originalname.includes('..')) {
throw new Error('Path traversal');
}
Дополнительно:
const normalized = path.normalize(file.originalname);
if (normalized.includes('..')) {
throw new Error('Недопустимый путь');
}
Иногда вместо загрузки передаётся ссылка на файл.
if (!validator.isURL(url)) {
throw new Error('Некорректный URL');
}
validator.isURL(url, {
protocols: ['https'],
require_protocol: true
});
Иногда система требует строгий формат:
my-avatar-01.png
const name = path.basename(file.originalname, ext);
if (!validator.isSlug(name)) {
throw new Error('Имя файла должно быть slug');
}
После загрузки может проверяться контрольная сумма.
validator.isMD5(hash);
validator.isHash(hash, 'sha1');
validator.isHash(hash, 'sha256');
const cleaned = validator.trim(file.originalname);
Удаление управляющих символов:
const cleaned = validator.stripLow(file.originalname);
const validator = require('validator');
const path = require('path');
function validateUpload(file) {
if (!file) {
throw new Error('Файл отсутствует');
}
if (!validator.isLength(file.originalname, {
min: 1,
max: 255
})) {
throw new Error('Некорректное имя');
}
if (!validator.isMimeType(file.mimetype)) {
throw new Error('Некорректный MIME');
}
const ext = path.extname(file.originalname).toLowerCase();
const allowedExt = ['.jpg', '.png'];
if (!allowedExt.includes(ext)) {
throw new Error('Расширение запрещено');
}
const allowedMime = [
'image/jpeg',
'image/png'
];
if (!allowedMime.includes(file.mimetype)) {
throw new Error('Тип файла запрещён');
}
if (file.size > 5 * 1024 * 1024) {
throw new Error('Слишком большой файл');
}
if (file.originalname.includes('..')) {
throw new Error('Опасный путь');
}
return true;
}
npm install express multer validator
const multer = require('multer');
const upload = multer({
dest: 'uploads/'
});
function validateFile(req, res, next) {
try {
validateUpload(req.file);
next();
} catch (err) {
res.status(400).json({
error: err.message
});
}
}
app.post(
'/upload',
upload.single('file'),
validateFile,
(req, res) => {
res.json({
success: true
});
}
);
Иногда требуется:
async function validateFileAsync(file) {
if (!validator.isMimeType(file.mimetype)) {
throw new Error('Некорректный MIME');
}
await antivirusScan(file.path);
return true;
}
Нельзя считать файл безопасным лишь потому, что он имеет:
.png
MIME легко подделывается клиентом.
Это приводит к:
Опасно:
uploads/${file.originalname}
Безопаснее:
crypto.randomUUID() + ext
SVG способен содержать:
Правильно:
allowed.includes(ext)
Неправильно:
!forbidden.includes(ext)
const crypto = require('crypto');
const filename =
crypto.randomUUID() + ext;
Нельзя сохранять пользовательские файлы прямо в:
/public/uploads
Надёжнее проверять:
Популярные решения:
Надёжная проверка файлов обычно состоит из нескольких уровней:
async function secureUploadValidation(file) {
if (!file) {
throw new Error('Файл отсутствует');
}
const ext = path.extname(file.originalname)
.toLowerCase();
const allowedExt = ['.jpg', '.png', '.pdf'];
if (!allowedExt.includes(ext)) {
throw new Error('Запрещённый формат');
}
if (!validator.isMimeType(file.mimetype)) {
throw new Error('Некорректный MIME');
}
if (file.size > 10 * 1024 * 1024) {
throw new Error('Файл слишком большой');
}
const safeFilename =
crypto.randomUUID() + ext;
await antivirusScan(file.path);
return {
valid: true,
filename: safeFilename
};
}