Back

Безопасная обработка пользовательского ввода в Node.js

Безопасная обработка пользовательского ввода в Node.js

Каждый API на Node.js получает данные, которым не может полностью доверять. JSON-тела запросов, строки запроса, поля форм и загружаемые файлы — всё это приходит из внешнего мира, и любой из этих источников может нести вредоносную нагрузку. SQL-инъекции, NoSQL-инъекции, инъекции команд, загрязнение прототипов — все эти атаки имеют общую точку входа: невалидированный пользовательский ввод, достигающий бизнес-логики, базы данных или команды оболочки. В этой статье рассматриваются практические паттерны, которые их останавливают.

Ключевые выводы

  • Всегда валидируйте входящие данные по строгой схеме, прежде чем они попадут в логику вашего приложения — определите, что разрешено, и отклоните всё остальное.
  • Используйте параметризованные запросы для SQL и применяйте строгую типизацию данных для NoSQL, чтобы предотвратить инъекции.
  • Избегайте child_process.exec() для системных команд; используйте вместо этого execFile() или spawn() с явными массивами аргументов.
  • Никогда не распространяйте необработанные тела запросов на модели; выбирайте только те поля, которые ожидаете, чтобы предотвратить массовое присваивание и загрязнение прототипов.
  • Устанавливайте ограничения на размер тела запроса и валидируйте загружаемые файлы на уровне парсера, чтобы защититься от атак типа «отказ в обслуживании».

Сначала валидация, потом обработка

Самое эффективное правило безопасной обработки запросов в Node.js API: никогда не позволяйте необработанным данным запроса касаться логики вашего приложения. Валидируйте форму и тип каждого входящего значения, прежде чем использовать его.

Валидация на основе схем — это наиболее чистый подход. Библиотеки вроде Zod и Joi позволяют точно объявить, как должен выглядеть запрос, и отклонить всё, что не соответствует.

import { z } from "zod"

const RegisterSchema = z.object({
  username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
  email: z.string().email(),
  password: z.string().min(10),
})

app.post("/register", (req, res) => {
  const result = RegisterSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() })
  }
  // result.data теперь безопасен для использования
})

Это подход белого списка: вы определяете, что разрешено, и всё остальное отклоняется. Это гораздо надёжнее, чем попытки создать чёрный список опасных символов. Тот же принцип подчёркивается в OWASP Input Validation Cheat Sheet.


Предотвращение инъекционных атак

SQL и NoSQL инъекции

Никогда не конкатенируйте пользовательский ввод в запросы. Используйте параметризованные выражения для SQL:

// ❌ Уязвимо
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`

// ✅ Безопасно
const result = await db.query("SELECT * FROM users WHERE email = $1", [email])

NoSQL базы данных имеют свой собственный риск инъекций. Операторы MongoDB вроде $gt могут быть внедрены через JSON-тела, если вы передаёте объекты напрямую:

// ❌ Уязвимо — req.body.username может быть { $gt: "" }
User.find({ username: req.body.username })

// ✅ Безопасно — сначала принудительно приведите к ожидаемому типу
User.find({ username: String(req.body.username) })

На практике валидация схемы должна гарантировать, что поле является строкой (например, с помощью z.string()), прежде чем оно достигнет уровня запросов.

Инъекции команд

Если ваш API вызывает системные команды, полностью избегайте child_process.exec() — он передаёт аргументы через оболочку и тривиально эксплуатируется. Используйте вместо этого execFile() или spawn() с явными массивами аргументов:

import { execFile } from "child_process"

// ✅ Аргументы передаются напрямую, не интерпретируются оболочкой
execFile("convert", [userSuppliedFilename, outputPath], (err, stdout) => {
  // обработка результата
})

Всё равно валидируйте и создавайте белый список значений аргументов. Если операция высокорисковая, выполняйте её внутри изолированного контейнера.


Избегайте массового присваивания и загрязнения прототипов

Массовое присваивание происходит, когда вы распространяете тело запроса напрямую на модель:

// ❌ Пользователь может отправить { role: "admin" }
const user = await User.create({ ...req.body })

// ✅ Выбирайте только те поля, которые ожидаете
const user = await User.create({
  username: result.data.username,
  email: result.data.email,
})

Загрязнение прототипов более тонкое. Слияние недоверенных объектов может позволить атакующему внедрить свойства вроде __proto__ или constructor.prototype, которые влияют на каждый объект в вашем процессе. Избегайте утилит глубокого слияния на необработанных данных запроса. Если вам необходимо слияние, сначала валидируйте ввод и создавайте промежуточные объекты с помощью Object.create(null), чтобы они не наследовались от цепочки прототипов по умолчанию. OWASP Prototype Pollution Prevention Cheat Sheet предоставляет дополнительные рекомендации.


Ограничьте размер и структуру запроса

Большие или глубоко вложенные нагрузки могут вызвать проблемы с отказом в обслуживании. Устанавливайте ограничения размера тела на уровне парсера:

app.use(express.json({ limit: "50kb" }))

Для загрузки файлов используйте multer с явными ограничениями fileSize и files, и валидируйте MIME-типы — никогда не доверяйте только расширению файла.


Заключение

Безопасная обработка пользовательского ввода в Node.js сводится к одному принципу: определите, как выглядит валидный ввод, примените это на границе и никогда не пропускайте ничего другого. Валидация схем, параметризованные запросы, явная передача аргументов и строгий выбор полей покрывают большинство реальных поверхностей атак. Применяйте эти паттерны последовательно на уровне запросов, прежде чем данные достигнут какой-либо последующей логики, и большинство уязвимостей класса инъекций просто не найдут места для внедрения.

Часто задаваемые вопросы

Валидация схемы — это сильная первая линия защиты, потому что она отклоняет некорректный ввод до того, как он достигнет вашей логики. Однако её следует комбинировать с параметризованными запросами и безопасным выполнением команд. Валидация гарантирует, что данные имеют правильную форму и тип, но последующий код также должен безопасно обрабатывать эти данные, чтобы покрыть граничные случаи, которые одна валидация может пропустить.

Обе библиотеки зрелые и широко используются. Zod предлагает первоклассную поддержку TypeScript с сильным выводом типов, что делает его естественным выбором для TypeScript-проектов. Joi имеет более длинную историю в экосистеме Express и предлагает богатый цепочечный API. Выбирайте на основе языковых предпочтений вашего проекта и существующих зависимостей.

Избегайте запуска функций глубокого слияния на необработанных данных запроса. Сначала валидируйте ввод со строгой схемой, чтобы проходили только известные поля. Когда слияние необходимо, создавайте промежуточные объекты с помощью Object.create(null) для получения объектов без цепочки прототипов. Библиотеки вроде lodash.merge имели уязвимости загрязнения прототипов в прошлом, поэтому всегда обновляйте зависимости.

Универсального ответа нет, но 100 КБ–1 МБ покрывает большинство случаев использования JSON API. Начните с консервативного лимита вроде 100 КБ и увеличивайте его только в том случае, если легитимные запросы требуют больше. Для загрузки файлов настройте отдельные лимиты через ваш multipart-парсер. Цель — отклонять аномально большие нагрузки на раннем этапе, прежде чем они потребляют память или процессорное время.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay