Валидация файлов загрузки

Валидация файлов загрузки отличается от проверки обычных строковых полей. Помимо текстовых значений необходимо учитывать:

  • имя файла;
  • расширение;
  • MIME-тип;
  • размер;
  • количество файлов;
  • безопасность содержимого;
  • наличие вредоносных символов;
  • корректность пути;
  • допустимые форматы.

Библиотека Validator.js работает только со строками, поэтому при проверке файлов обычно валидируются:

  • originalname
  • mimetype
  • путь к файлу
  • пользовательские метаданные

Фактическая проверка бинарного содержимого выполняется отдельно.


Установка Validator.js

npm install validator

Импорт в CommonJS:

const validator = require('validator');

Импорт в ES Modules:

import validator from 'validator';

Структура объекта файла

В Node.js файл часто приходит через middleware:

  • Multer
  • Busboy
  • Formidable

Пример объекта:

{
  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('Имя файла содержит запрещённые символы');
}

Проверка имени через whitelist

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&lt;script&gt;.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('Опасный тип файла');
}

Проверка MIME-типа

Проверка через Validator.js

if (!validator.isMimeType(file.mimetype)) {
  throw new Error('Некорректный MIME-тип');
}

Ограничение допустимых MIME-типов

const allowedMime = [
  'image/jpeg',
  'image/png',
  'image/webp'
];

if (!allowedMime.includes(file.mimetype)) {
  throw new Error('Недопустимый MIME-тип');
}

Проблема подмены MIME-типа

Клиент может отправить:

image/png

для файла:

virus.exe

Поэтому MIME-тип нельзя считать надёжным источником.

Для безопасной проверки используют анализ сигнатур файла:

  • file-type
  • magic numbers
  • антивирусные сканеры

Проверка размера файла

Validator.js не валидирует числа напрямую, поэтому размер преобразуется в строку либо проверяется обычным JavaScript.

Проверка через JavaScript

const MAX_SIZE = 5 * 1024 * 1024;

if (file.size > MAX_SIZE) {
  throw new Error('Файл слишком большой');
}

Проверка через Validator.js

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'
];

Проверка MIME-типов изображений

const imageMime = [
  'image/jpeg',
  'image/png',
  'image/gif',
  'image/webp'
];

if (!imageMime.includes(file.mimetype)) {
  throw new Error('Недопустимое изображение');
}

Проверка SVG

SVG-файлы опасны возможностью внедрения Jav * aScript:

<script>alert(1)</script>

Поэтому SVG требует дополнительной очистки.

Проверка:

if (file.mimetype === 'image/svg+xml') {
  throw new Error('SVG запрещён');
}

Либо использовать sanitizer.


Валидация PDF-файлов

Проверка расширения

if (ext !== '.pdf') {
  throw new Error('Только PDF');
}

Проверка MIME-типа

if (file.mimetype !== 'application/pdf') {
  throw new Error('Некорректный PDF');
}

Проверка сигнатуры 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');
}

Валидация архивов

Архивы особенно опасны:

  • ZIP bomb
  • path traversal
  • скрытые исполняемые файлы

Ограничение типов архивов

const allowedArchives = [
  '.zip',
  '.rar',
  '.7z'
];

Запрет tar и gz

Некоторые серверы запрещают:

const forbidden = ['.tar', '.gz'];

if (forbidden.includes(ext)) {
  throw new Error('Архив запрещён');
}

Path Traversal

Опасные имена

../. ./. ./etc/passwd

Защита

if (file.originalname.includes('..')) {
  throw new Error('Path traversal');
}

Дополнительно:

const normalized = path.normalize(file.originalname);

if (normalized.includes('..')) {
  throw new Error('Недопустимый путь');
}

Проверка URL-файлов

Иногда вместо загрузки передаётся ссылка на файл.

Проверка URL

if (!validator.isURL(url)) {
  throw new Error('Некорректный URL');
}

Ограничение протоколов

validator.isURL(url, {
  protocols: ['https'],
  require_protocol: true
});

Проверка имени файла как slug

Иногда система требует строгий формат:

my-avatar-01.png

Использование isSlug

const name = path.basename(file.originalname, ext);

if (!validator.isSlug(name)) {
  throw new Error('Имя файла должно быть slug');
}

Валидация хэшей файлов

После загрузки может проверяться контрольная сумма.

MD5

validator.isMD5(hash);

SHA1

validator.isHash(hash, 'sha1');

SHA256

validator.isHash(hash, 'sha256');

Очистка пользовательских путей

trim

const cleaned = validator.trim(file.originalname);

stripLow

Удаление управляющих символов:

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;
}

Интеграция с Express и Multer

Установка

npm install express multer validator

Настройка Multer

const multer = require('multer');

const upload = multer({
  dest: 'uploads/'
});

Middleware валидации

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-типу

MIME легко подделывается клиентом.


Отсутствие ограничения размера

Это приводит к:

  • переполнению диска;
  • отказу в обслуживании;
  • перегрузке памяти.

Сохранение оригинального имени

Опасно:

uploads/${file.originalname}

Безопаснее:

crypto.randomUUID() + ext

Отсутствие проверки SVG

SVG способен содержать:

  • JavaScript;
  • внешние ссылки;
  • XSS-полезную нагрузку.

Рекомендации по безопасности

Использование белых списков

Правильно:

allowed.includes(ext)

Неправильно:

!forbidden.includes(ext)

Генерация безопасных имён

const crypto = require('crypto');

const filename =
  crypto.randomUUID() + ext;

Хранение файлов вне public

Нельзя сохранять пользовательские файлы прямо в:

/public/uploads

Проверка сигнатур

Надёжнее проверять:

  • magic bytes;
  • file signatures;
  • бинарные заголовки.

Использование антивирусов

Популярные решения:

  • ClamAV
  • VirusTotal API
  • sandbox-анализ

Комбинированная стратегия валидации

Надёжная проверка файлов обычно состоит из нескольких уровней:

  1. Проверка имени файла.
  2. Проверка длины имени.
  3. Проверка расширения.
  4. Проверка MIME-типа.
  5. Проверка размера.
  6. Проверка сигнатуры.
  7. Генерация безопасного имени.
  8. Антивирусное сканирование.
  9. Ограничение числа файлов.
  10. Хранение вне публичной директории.

Пример промышленной схемы

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
  };
}