Этот документ призван расширить текущую модель угроз и дать подробные рекомендации о том, как защитить приложение на Node.js.
- Лучшие практики: упрощённый сжатый взгляд на лучшие практики. За отправную точку можно взять этот issue или эти рекомендации. Важно отметить, что этот документ специфичен для Node.js; если вам нужно что-то более широкое, обратите внимание на OSSF Best Practices.
- Разбор атак: наглядно и простым языком, с примерами кода (по возможности), документируем атаки, упомянутые в модели угроз.
- Сторонние библиотеки: описываем угрозы (атаки typosquatting, вредоносные пакеты…) и лучшие практики работы с зависимостями node-модулей и т. д.
Модель угроз Node.js определяет, что считается и что не считается уязвимостью в самом Node.js. Некоторые из тем ниже не являются уязвимостями ядра Node.js согласно этой модели, но всё равно остаются важными угрозами на уровне приложения, которые нужно учитывать при разработке и эксплуатации ПО на Node.js.
Это атака, при которой приложение перестаёт выполнять то, для чего оно было спроектировано, из-за того, как оно обрабатывает входящие HTTP-запросы. Эти запросы не обязательно должны быть намеренно сконструированы злоумышленником: неправильно настроенный или содержащий баг клиент тоже может отправлять серверу такой поток запросов, который приводит к отказу в обслуживании.
HTTP-запросы принимаются HTTP-сервером Node.js и передаются коду приложения через зарегистрированный обработчик запросов. Сервер не парсит содержимое тела запроса. Поэтому любой DoS, вызванный содержимым тела после его передачи обработчику запросов, не является уязвимостью самого Node.js, поскольку за корректную обработку отвечает код приложения.
Убедитесь, что веб-сервер корректно обрабатывает ошибки сокетов: например, если сервер создан без обработчика ошибок, он будет уязвим к DoS.
const net = require('node:net');
const server = net.createServer(function (socket) {
// socket.on('error', console.error) // это предотвращает падение сервера
socket.write('Echo server\r\n');
socket.pipe(socket);
});
server.listen(5000, '0.0.0.0');Если выполнить некорректный запрос, сервер может упасть.
Пример DoS-атаки, не связанной с содержимым запроса, — Slowloris. В этой атаке HTTP-запросы отправляются медленно и фрагментами, по одному фрагменту за раз. Пока полный запрос не доставлен, сервер будет удерживать ресурсы, выделенные под текущий запрос. Если отправить достаточно много таких запросов одновременно, число одновременных соединений вскоре достигнет максимума, что приведёт к отказу в обслуживании. Так эта атака зависит не от содержимого запроса, а от тайминга и характера отправляемых серверу запросов.
Меры защиты
- Используйте обратный прокси (reverse proxy) для приёма и перенаправления запросов к приложению Node.js. Обратные прокси могут обеспечивать кэширование, балансировку нагрузки, блокировку IP и т. п., что снижает вероятность успешной DoS-атаки.
- Правильно настройте таймауты сервера, чтобы соединения, которые простаивают или
по которым запросы приходят слишком медленно, можно было сбрасывать. См. различные таймауты
в
http.Server, в частностиheadersTimeout,requestTimeout,timeoutиkeepAliveTimeout. - Ограничьте число открытых сокетов на хост и в целом. См. документацию http,
в частности
agent.maxSockets,agent.maxTotalSockets,agent.maxFreeSocketsиserver.maxRequestsPerSocket.
Это атака, которая может нацеливаться на приложения Node.js, запущенные с включённым отладочным inspector через ключ --inspect.
Поскольку сайты, открытые в веб-браузере, могут делать WebSocket- и HTTP-запросы, они могут нацелиться на отладочный inspector, работающий локально. Обычно этому препятствует политика одного источника (same-origin), реализованная в современных браузерах, которая запрещает скриптам обращаться к ресурсам из других источников (то есть вредоносный сайт не может прочитать данные, запрошенные с локального IP-адреса).
Однако с помощью DNS rebinding злоумышленник может временно контролировать источник своих запросов так, чтобы они выглядели исходящими с локального IP-адреса. Это достигается контролем и над сайтом, и над DNS-сервером, используемым для разрешения его IP-адреса. Подробнее см. вики про DNS Rebinding.
Меры защиты
- Отключайте inspector по сигналу SIGUSR1, повесив на него слушатель
process.on('SIGUSR1', …). - Не запускайте протокол inspector в production.
Все файлы и папки, находящиеся в текущей директории, отправляются в реестр npm при публикации пакета.
Есть механизмы для управления этим поведением: можно задать блок-лист через
.npmignore и .gitignore или задать allow-лист в package.json.
Меры защиты
- Используйте
npm publish --dry-run, чтобы вывести список всех публикуемых файлов. Обязательно просмотрите содержимое перед публикацией пакета. - Также важно создавать и поддерживать ignore-файлы, такие как
.gitignoreи.npmignore. В этих файлах можно указать, какие файлы/папки не должны публиковаться. Свойство files вpackage.jsonпозволяет выполнить обратную операцию — allow-лист. - В случае утечки обязательно снимите пакет с публикации.
Это атака, в которой участвуют два HTTP-сервера (обычно прокси и приложение Node.js). Клиент отправляет HTTP-запрос, который сначала проходит через фронтенд-сервер (прокси), а затем перенаправляется на бэкенд-сервер (приложение). Когда фронтенд и бэкенд по-разному интерпретируют неоднозначные HTTP-запросы, у злоумышленника появляется возможность отправить вредоносное сообщение, которое не будет замечено фронтендом, но будет замечено бэкендом, — фактически «протащив» его мимо прокси-сервера.
Более подробное описание и примеры см. в CWE-444.
Поскольку эта атака зависит от того, что Node.js интерпретирует HTTP-запросы иначе, чем (произвольный) HTTP-сервер, успешная атака может быть следствием уязвимости в Node.js, во фронтенд-сервере или в обоих. Если то, как Node.js интерпретирует запрос, согласуется со спецификацией HTTP (см. RFC7230), то это не считается уязвимостью в Node.js.
Меры защиты
- Не используйте опцию
insecureHTTPParserпри создании HTTP-сервера. - Настройте фронтенд-сервер на нормализацию неоднозначных запросов.
- Непрерывно отслеживайте новые уязвимости HTTP request smuggling как в Node.js, так и в выбранном фронтенд-сервере.
- По возможности используйте HTTP/2 сквозным образом (end to end) и отключайте понижение до HTTP (HTTP downgrading).
Это атака, которая позволяет злоумышленнику узнать потенциально конфиденциальную информацию, например измеряя, сколько времени приложению требуется на ответ на запрос. Эта атака не специфична для Node.js и может нацеливаться почти на любые среды выполнения.
Атака возможна всякий раз, когда приложение использует секрет в операции, чувствительной к таймингу (например, в ветвлении). Рассмотрим обработку аутентификации в типичном приложении. Здесь базовый метод аутентификации использует email и пароль в качестве учётных данных. Информация о пользователе извлекается из введённых пользователем данных, в идеале из СУБД. После извлечения информации о пользователе пароль сравнивается с информацией о пользователе, полученной из базы данных. Встроенное сравнение строк занимает больше времени для значений одинаковой длины. Это сравнение при выполнении на приемлемом объёме данных невольно увеличивает время ответа на запрос. Сравнивая времена ответа на запросы, злоумышленник может угадать длину и значение пароля за большое количество запросов.
Меры защиты
-
API crypto предоставляет функцию
timingSafeEqualдля сравнения фактического и ожидаемого конфиденциальных значений с помощью алгоритма постоянного времени. -
Для сравнения паролей можно использовать scrypt, также доступный в нативном модуле crypto.
-
В более общем случае избегайте использования секретов в операциях с переменным временем. Сюда относится ветвление на основе секретов и — когда злоумышленник может находиться на той же инфраструктуре (например, на той же облачной машине) — использование секрета как индекса в памяти. Писать код с постоянным временем на JavaScript трудно (отчасти из-за JIT). Для криптографических приложений используйте встроенные API crypto или WebAssembly (для алгоритмов, не реализованных нативно).
Согласно модели угроз Node.js, сценарии, требующие вредоносного стороннего модуля, не считаются уязвимостями ядра Node.js, потому что Node.js рассматривает код, который его просят выполнить (включая зависимости), как доверенный. Однако вредоносные или скомпрометированные зависимости остаются одним из наиболее критичных рисков на уровне приложения для пользователей Node.js и должны восприниматься как таковые.
Сейчас в Node.js любой пакет может обращаться к мощным ресурсам, таким как доступ к сети. Более того, поскольку у них также есть доступ к файловой системе, они могут отправить любые данные куда угодно.
Весь код, работающий в процессе node, способен загружать и выполнять дополнительный
произвольный код с помощью eval() (или его эквивалентов).
Весь код с доступом на запись в файловую систему может достичь того же, записывая в
новые или существующие файлы, которые затем загружаются.
Примеры
- Злоумышленник компрометирует аккаунт сопровождающего популярной библиотеки логирования и выпускает новую минорную версию, которая при инициализации логгера эксфильтрует переменные окружения (например, пароли к базе данных или access-токены) на удалённый сервер.
- В реестр npm публикуется typosquatting-пакет с именем, похожим на имя известного фреймворка. При установке он выполняет postinstall-скрипт, который отправляет SSH-ключи с машины разработчика на подконтрольный злоумышленнику эндпоинт.
Обязательно фиксируйте (pin) версии зависимостей и запускайте автоматические проверки на уязвимости с помощью распространённых workflow или npm-скриптов. Перед установкой пакета убедитесь, что он сопровождается и содержит именно то, что вы ожидали. Будьте осторожны: исходный код на GitHub не всегда совпадает с опубликованным, проверяйте его в node_modules.
Атака на цепочку поставок для приложения Node.js происходит, когда одна из его зависимостей (прямая или транзитивная) скомпрометирована. Это может случиться из-за того, что приложение слишком нестрого задаёт зависимости (допуская нежелательные обновления), и/или из-за типичных опечаток в их указании (уязвимость к typosquatting).
Злоумышленник, получивший контроль над вышестоящим пакетом, может опубликовать новую версию с вредоносным кодом. Если приложение Node.js зависит от этого пакета, не будучи строгим в том, какую версию безопасно использовать, пакет может автоматически обновиться до последней вредоносной версии, скомпрометировав приложение.
Зависимости, указанные в файле package.json, могут иметь точный номер версии
или диапазон. Однако при фиксации зависимости на точную версию её
транзитивные зависимости сами по себе не фиксируются.
Это по-прежнему оставляет приложение уязвимым к нежелательным/неожиданным обновлениям.
Возможные векторы атаки:
- Атаки typosquatting
- Отравление lock-файла (lockfile poisoning)
- Скомпрометированные сопровождающие
- Вредоносные пакеты
- Путаница зависимостей (dependency confusion)
Меры защиты
- Запретите npm выполнять произвольные скрипты с помощью
--ignore-scripts- Дополнительно можно отключить это глобально:
npm config set ignore-scripts true
- Дополнительно можно отключить это глобально:
- Фиксируйте версии зависимостей на конкретной неизменяемой версии, а не на диапазоне или на изменяемом источнике.
- Используйте lock-файлы, которые фиксируют каждую зависимость (прямую и транзитивную).
- Применяйте меры защиты от отравления lock-файла.
- Автоматизируйте проверки новых уязвимостей с помощью CI, инструментами вроде
npm-audit.- Инструменты вроде
Socketможно использовать для статического анализа пакетов, чтобы находить рискованное поведение, например доступ к сети или файловой системе.
- Инструменты вроде
- Используйте
npm ciвместоnpm install. Это принудительно применяет lock-файл, так что несоответствия между ним и файлом package.json приводят к ошибке (вместо тихого игнорирования lock-файла в пользу package.json). - Внимательно проверяйте файл package.json на ошибки/опечатки в именах зависимостей.
- Задайте «выдержку» зависимостей через
--min-release-age(npm v11.10.0+), чтобы не устанавливать недавно опубликованные пакеты. Значение задаётся в днях (например,1означает, что пакеты должны быть не моложе одного дня). Большинство скомпрометированных пакетов обнаруживают и удаляют в течение часов. Даже однодневная выдержка исключает подверженность большинству короткоживущих атак на цепочку поставок:Чтобы применить исправления безопасности, не дожидаясь выдержки, переопределяйте её для конкретной команды:min-release-age=1npm install package-name --min-release-age=0. Используйтеnpm audit, чтобы выявить пакеты с известными уязвимостями, требующими немедленного обновления.
Атаки на память или кучу (heap) зависят от сочетания ошибок управления памятью и эксплуатируемого аллокатора памяти. Как и все среды выполнения, Node.js уязвим к этим атакам, если ваши проекты работают на общей машине. Использование защищённой кучи (secure heap) полезно, чтобы предотвратить утечку конфиденциальной информации из-за выходов за границы указателей (overruns и underruns).
К сожалению, защищённая куча недоступна в Windows. Подробнее см. в документации по secure-heap Node.js.
Меры защиты
- Используйте
--secure-heap=nв зависимости от вашего приложения, где n — выделяемый максимальный размер в байтах. - Не запускайте ваше production-приложение на общей машине.
Monkey patching — это изменение свойств во время выполнения с целью поменять существующее поведение. Пример:
Array.prototype.push = function (item) {
// переопределение глобального [].push
};Меры защиты
Флаг --frozen-intrinsics включает экспериментальные¹
замороженные интринсики (frozen intrinsics), что означает: все встроенные объекты и функции JavaScript
рекурсивно замораживаются.
Поэтому следующий сниппет не будет переопределять поведение по умолчанию
Array.prototype.push
Array.prototype.push = function (item) {
// переопределение глобального [].push
};
// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object ''Однако важно отметить, что вы всё ещё можете определять новые глобальные значения и заменять
существующие через globalThis
> globalThis.foo = 3; foo; // новые глобальные значения по-прежнему можно определять
3
> globalThis.Array = 4; Array; // однако существующие глобальные значения тоже можно заменять
4Поэтому Object.freeze(globalThis) можно использовать, чтобы гарантировать, что никакие глобальные значения не будут
заменены.
Согласно модели угроз Node.js, prototype pollution, опирающаяся на контроль пользовательского ввода злоумышленником, не считается уязвимостью ядра Node.js, потому что Node.js доверяет входным данным, предоставленным кодом приложения. Тем не менее prototype pollution — это серьёзный класс уязвимостей для приложений Node.js и сторонних библиотек, и вам следует реализовывать защиту на уровне приложения и зависимостей.
Prototype pollution — это возможность изменять или внедрять свойства в элементы языка Javascript, злоупотребляя использованием __proto__, _constructor_, prototype и других свойств, унаследованных от встроенных прототипов.
const a = { a: 1, b: 2 };
const data = JSON.parse('{"__proto__": { "polluted": true}}');
const c = Object.assign({}, a, data);
console.log(c.polluted); // true
// Потенциальный DoS
const data2 = JSON.parse('{"__proto__": null}');
const d = Object.assign(a, data2);
d.hasOwnProperty('b'); // Uncaught TypeError: d.hasOwnProperty is not a functionЭто потенциальная уязвимость, унаследованная от языка JavaScript.
Примеры:
- CVE-2022-21824 (Node.js)
- CVE-2018-3721 (сторонняя библиотека: Lodash)
Дополнительные сценарии:
- Веб-API без валидации сливает недоверенные JSON-тела запросов в общий
объект конфигурации. Отправив полезную нагрузку со свойством
__proto__, злоумышленник добавляет неожиданные свойства ко многим объектам в процессе, что приводит к логическим багам или отказу в обслуживании. - Сервис рендеринга шаблонов принимает управляемые пользователем опции и передаёт их
напрямую в утилиту глубокого слияния (deep merge). Загрязнив
Object.prototype, злоумышленник заставляет все будущие шаблоны вести себя непредсказуемо, потенциально обходя проверки безопасности, которые опираются на наличие свойств объекта.
Меры защиты
- Избегайте небезопасных рекурсивных слияний, см. CVE-2018-16487.
- Реализуйте валидацию по JSON Schema для внешних/недоверенных запросов.
- Создавайте объекты без прототипа с помощью
Object.create(null). - Замораживайте прототип:
Object.freeze(MyObject.prototype). - Отключите свойство
Object.prototype.__proto__с помощью флага--disable-proto. - Проверяйте, что свойство существует непосредственно на объекте, а не унаследовано от прототипа,
с помощью
Object.hasOwn(obj, keyFromObj). - Избегайте использования методов из
Object.prototype.
Модель угроз Node.js считает файловую систему в окружении, доступном Node.js, доверенной. В результате проблемы, которые опираются исключительно на контроль файлов в этих местах, не считаются уязвимостями в ядре Node.js. Однако они важны для безопасности всего вашего развёртывания и цепочки поставок, поэтому вам следует укреплять окружение и использовать приведённые ниже механизмы, чтобы снизить риск.
Node.js загружает модули по алгоритму разрешения модулей. Поэтому он предполагает, что директория, в которой запрашивается модуль (require), доверенная.
Под этим подразумевается, что следующее поведение приложения ожидаемо. Предположим следующую структуру директорий:
- app/
- server.js
- auth.js
- auth
Если server.js использует require('./auth'), он последует алгоритму разрешения модулей
и загрузит auth вместо auth.js.
Node.js предоставляет модель разрешений (permission model), с помощью которой можно ограничить, что данному процессу разрешено делать во время выполнения. Эта модель дополняет модель угроз Node.js.
Когда она включена (например, флагом --permission),
модель разрешений позволяет выборочно разрешать или запрещать доступ к чувствительным
возможностям, таким как:
- Чтение и запись файловой системы.
- Доступ к сети (входящий и исходящий).
- Создание дочерних процессов.
- Использование нативных аддонов и других мощных API.
Это помогает ограничить влияние вредоносных или скомпрометированных зависимостей, недоверенной конфигурации или неожиданного поведения в вашем собственном коде, поскольку даже доверенному коду будет запрещено выполнять действия за пределами разрешений, которые вы явно предоставили.
Актуальные флаги и опции см. в документации по разрешениям Node.js.
Использовать экспериментальные возможности в production не рекомендуется. Экспериментальные возможности при необходимости могут получать ломающие изменения, и их функциональность не является надёжно стабильной. Тем не менее обратная связь очень приветствуется.
OpenSSF ведёт несколько инициатив, которые могут быть очень полезны, особенно если вы планируете публиковать npm-пакет. К этим инициативам относятся:
- OpenSSF Scorecard Scorecard оценивает open source проекты с помощью серии автоматизированных проверок рисков безопасности. С его помощью можно проактивно оценивать уязвимости и зависимости в вашей кодовой базе и принимать осознанные решения о допустимости уязвимостей.
- Программа значков OpenSSF Best Practices Проекты могут добровольно самосертифицироваться, описывая, как они соответствуют каждой лучшей практике. По итогам генерируется значок, который можно добавить в проект.
Вы также можете сотрудничать с другими проектами и экспертами по безопасности через OpenJS Security Collaboration Space.