Валидация MIME типов

MIME-тип (Multipurpose Internet Mail Extensions) — строковый идентификатор формата данных, который используется для описания содержимого файла или HTTP-сообщения. MIME-тип позволяет определить, как браузер, сервер или приложение должны интерпретировать данные.

Примеры MIME-типов:

MIME-тип Описание
text/plain Обычный текст
text/html HTML-документ
application/json JSON-данные
image/png PNG-изображение
image/jpeg JPEG-изображение
audio/mpeg MP3-аудио
video/mp4 MP4-видео

Валидация MIME-типов применяется:

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

Validator.js предоставляет функцию isMimeType(), предназначенную для проверки корректности MIME-строки.


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

npm install validator

Подключение библиотеки:

const validator = require('validator');

Для ES Modules:

import validator from 'validator';

Метод isMimeType()

Сигнатура

validator.isMimeType(str)

Параметры

Параметр Тип Описание
str string Проверяемая MIME-строка

Возвращаемое значение

Тип Описание
boolean true, если MIME-тип корректен

Базовая проверка MIME-типов

Корректные значения

validator.isMimeType('text/plain');
true

validator.isMimeType('application/json');
true

validator.isMimeType('image/png');
true

Некорректные MIME-типы

validator.isMimeType('plain/text');
false

validator.isMimeType('image');
false

validator.isMimeType('application/');
false

validator.isMimeType('/json');
false

Структура MIME-типа

MIME-тип состоит из двух частей:

type/subtype

Пример:

image/jpeg

Где:

Часть Значение
image основной тип
jpeg подтип

Основные категории MIME-типов

Текстовые данные

text/plain
text/html
text/css
text/csv

Изображения

image/png
image/jpeg
image/gif
image/webp

Видео

video/mp4
video/webm
video/ogg

Аудио

audio/mpeg
audio/wav
audio/ogg

Двоичные данные

application/json
application/pdf
application/zip
application/xml

Проверка MIME при загрузке файлов

Express + Multer

npm install express multer validator

Пример middleware

const express = require('express');
const multer = require('multer');
const validator = require('validator');

const app = express();

const upload = multer({
    storage: multer.memoryStorage(),

    fileFilter(req, file, cb) {

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

        cb(null, true);
    }
});

app.post('/upload', upload.single('file'), (req, res) => {
    res.send('Файл загружен');
});

Ограничение допустимых типов файлов

Проверка только корректности MIME-типа недостаточна. Необходимо ограничивать список разрешённых значений.

Разрешение только изображений

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

function isAllowedMimeType(mime) {
    return (
        validator.isMimeType(mime) &&
        allowedMimeTypes.includes(mime)
    );
}

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

function isPdf(mime) {
    return mime === 'application/pdf';
}

Проверка изображений

function isImage(mime) {

    if (!validator.isMimeType(mime)) {
        return false;
    }

    return mime.startsWith('image/');
}

Проверка аудио

function isAudio(mime) {

    if (!validator.isMimeType(mime)) {
        return false;
    }

    return mime.startsWith('audio/');
}

Проверка видео

function isVideo(mime) {

    if (!validator.isMimeType(mime)) {
        return false;
    }

    return mime.startsWith('video/');
}

Использование регулярных выражений

Иногда MIME-типы проверяются через RegExp:

const imageMimeRegex = /^image\/.+$/;

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

imageMimeRegex.test('image/png');
true

Однако Validator.js обеспечивает дополнительную базовую проверку структуры MIME-типа.


Комбинированная проверка

function validateImageMime(mime) {

    if (!validator.isMimeType(mime)) {
        return false;
    }

    return /^image\/(png|jpeg|webp|gif)$/.test(mime);
}

Валидация HTTP-заголовка Content-Type

HTTP-заголовок:

Content-Type: application/json

Проверка:

const contentType = req.headers['content-type'];

if (!validator.isMimeType(contentType)) {
    throw new Error('Некорректный Content-Type');
}

Проблема параметров charset

Многие заголовки содержат параметры:

Content-Type: text/html; charset=utf-8

validator.isMimeType() проверяет только MIME-тип, поэтому строка с параметрами вернёт false.

Неправильно

validator.isMimeType('text/html; charset=utf-8');
false

Извлечение MIME-типа из заголовка

Решение через split

function extractMimeType(contentType) {

    if (!contentType) {
        return '';
    }

    return contentType.split(';')[0].trim();
}

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

const mime = extractMimeType(
    'text/html; charset=utf-8'
);

validator.isMimeType(mime);
true

Валидация Content-Type в API

app.use((req, res, next) => {

    const contentType = req.headers['content-type'];

    if (!contentType) {
        return res.status(400).send('Content-Type обязателен');
    }

    const mime = contentType.split(';')[0];

    if (!validator.isMimeType(mime)) {
        return res.status(400).send('Некорректный Content-Type');
    }

    next();
});

Проверка MIME в NestJS

Pipe для проверки

import {
    PipeTransform,
    Injectable,
    BadRequestException
} from '@nestjs/common';

import validator from 'validator';

@Injectable()
export class MimeTypePipe
implements PipeTransform {

    transform(value) {

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

        return value;
    }
}

Проверка в Fastify

fastify.addHook('preHandler', async (request) => {

    const contentType =
        request.headers['content-type'];

    if (!contentType) {
        throw new Error('Content-Type отсутствует');
    }

    const mime = contentType.split(';')[0];

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

MIME и безопасность

Проверка MIME-типов играет важную роль в безопасности приложений.

Основные угрозы

Угроза Описание
Подмена расширения virus.exeimage.jpg
Загрузка исполняемых файлов .exe, .bat, .sh
XSS через HTML загрузка HTML вместо изображения
SVG-инъекции вредоносный JavaScript внутри SVG
Маскировка контента поддельный Content-Type

MIME нельзя считать полностью надёжным

MIME-тип часто приходит:

  • из браузера;
  • из HTTP-заголовков;
  • из имени файла;
  • из клиента.

Следовательно, MIME может быть подделан.


Проверка сигнатуры файла

Для критически важных систем MIME следует проверять совместно с сигнатурой файла.

Пример библиотеки file-type

npm install file-type

Проверка реального типа файла

const { fileTypeFromBuffer } = require('file-type');

async function validateFile(fileBuffer) {

    const result =
        await fileTypeFromBuffer(fileBuffer);

    if (!result) {
        return false;
    }

    return result.mime === 'image/png';
}

Сравнение MIME и расширения

const path = require('path');

function validateFile(file) {

    const ext =
        path.extname(file.originalname);

    const mime =
        file.mimetype;

    return (
        ext === '.png' &&
        mime === 'image/png'
    );
}

Белый список MIME-типов

Хорошая практика

const allowed = new Set([
    'image/png',
    'image/jpeg',
    'application/pdf'
]);

function validateMime(mime) {

    return (
        validator.isMimeType(mime) &&
        allowed.has(mime)
    );
}

Чёрный список MIME-типов

Менее безопасный подход:

const forbidden = [
    'application/x-msdownload',
    'application/x-sh'
];

Причина — невозможно предусмотреть все опасные типы.


Проверка multipart/form-data

При отправке форм браузер использует:

multipart/form-data

Проверка:

validator.isMimeType('multipart/form-data');
true

Проверка JSON API

function validateJsonRequest(req) {

    const contentType =
        req.headers['content-type'];

    if (!contentType) {
        return false;
    }

    const mime =
        contentType.split(';')[0];

    return mime === 'application/json';
}

Проверка XML

validator.isMimeType('application/xml');
true

Проверка нестандартных MIME-типов

Validator.js поддерживает и vendor-specific MIME-типы.

Пример

validator.isMimeType(
    'application/vnd.api+json'
);
true

MIME-типы с суффиксами

Современные API активно используют суффиксы:

application/ld+json
application/problem+json
application/vnd.api+json

Проверка:

validator.isMimeType(
    'application/problem+json'
);
true

Массовая проверка MIME-типов

const mimeTypes = [
    'image/png',
    'application/json',
    'invalid/type/'
];

const result = mimeTypes.map(mime => ({
    mime,
    valid: validator.isMimeType(mime)
}));

console.log(result);

Валидация массива файлов

function validateFiles(files) {

    return files.every(file => {

        return validator.isMimeType(
            file.mimetype
        );
    });
}

Проверка MIME в GraphQL Upload

async function uploadFile(_, { file }) {

    const { mimetype } = await file;

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

    return true;
}

Типичные ошибки

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

Плохо:

if (filename.endsWith('.png'))

Расширение можно подделать.


Доверие Content-Type

Плохо:

req.headers['content-type']

Заголовок легко изменить вручную.


Отсутствие whitelist

Плохо:

if (validator.isMimeType(mime))

Корректный MIME ещё не означает безопасный файл.


Рекомендуемая схема проверки файлов

Этап 1 — Проверка MIME

validator.isMimeType(mime)

Этап 2 — Проверка whitelist

allowed.has(mime)

Этап 3 — Проверка сигнатуры файла

fileTypeFromBuffer(buffer)

Этап 4 — Проверка размера

if (file.size > MAX_SIZE)

Этап 5 — Антивирусная проверка

ClamAV, VirusTotal, Sandbox и другие решения.


Производительность

isMimeType() работает очень быстро, поскольку выполняет только строковую проверку формата.

Даже массовая валидация тысяч MIME-типов обычно не создаёт заметной нагрузки.


Ограничения isMimeType()

Функция:

  • не проверяет существование реального формата файла;
  • не анализирует содержимое;
  • не определяет безопасность файла;
  • не валидирует параметры (charset, boundary);
  • не гарантирует отсутствие подделки MIME.

Практический middleware для Express

const validator = require('validator');

const allowedMimeTypes = new Set([
    'image/png',
    'image/jpeg',
    'application/pdf'
]);

function validateMime(req, res, next) {

    const file = req.file;

    if (!file) {
        return res.status(400).send(
            'Файл отсутствует'
        );
    }

    const mime = file.mimetype;

    if (!validator.isMimeType(mime)) {

        return res.status(400).send(
            'Некорректный MIME'
        );
    }

    if (!allowedMimeTypes.has(mime)) {

        return res.status(400).send(
            'Тип файла запрещён'
        );
    }

    next();
}

Практический helper

import validator from 'validator';

export function isAllowedMime(
    mime,
    allowed
) {

    if (!mime) {
        return false;
    }

    if (!validator.isMimeType(mime)) {
        return false;
    }

    return allowed.includes(mime);
}

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

const allowed = [
    'image/png',
    'image/jpeg'
];

isAllowedMime(
    'image/png',
    allowed
);
true