Рекомендации по обеспечению безопасности
Цель
Настоящий документ призван расширить существующую модель угроз и предоставить подробные рекомендации по обеспечению безопасности приложения Node.js.
Содержание документа
- Рекомендации по обеспечению безопасности: упрощенный краткий обзор лучших практик. В качестве отправной точки можно использовать этот вопрос или это руководство. Важно отметить, что данный документ относится конкретно к Node.js; если вам нужно что-то более общее, обратитесь к рекомендациям OSSF.
- Описание атак: иллюстрация и описание на простом английском языке с примерами кода (если возможно) атак, упомянутых в модели угроз.
- Сторонние библиотеки: определение угроз (атаки типа typosquatting, вредоносные пакеты и т.д.) и рекомендации по обеспечению безопасности зависимостей модулей Node и т.д.
Список угроз
Отказ в обслуживании HTTP-сервера (CWE-400)
Это атака, при которой приложение становится недоступным для выполнения своего предназначения из-за способа обработки входящих HTTP-запросов. Эти запросы необязательно должны быть специально созданы злоумышленником: некорректно настроенный или содержащий ошибки клиент также может отправлять на сервер последовательность запросов, приводящих к отказу в обслуживании.
HTTP-запросы принимаются HTTP-сервером Node.js и передаются коду приложения через зарегистрированный обработчик запросов. Сервер не анализирует содержимое тела запроса. Поэтому любой отказ в обслуживании, вызванный содержимым тела после его передачи обработчику запросов, не является уязвимостью самого Node.js, поскольку за его правильную обработку отвечает код приложения.
Убедитесь, что веб-сервер правильно обрабатывает ошибки сокетов; например, если сервер создан без обработчика ошибок, он будет уязвим для атак типа DoS.
import net from 'node:net'
const server = net.createServer(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-запросы отправляются медленно и фрагментировано, по одному фрагменту за раз. До тех пор, пока не будет доставлен полный запрос, сервер будет резервировать ресурсы для текущего запроса. Если одновременно будет отправлено достаточно таких запросов, количество одновременных подключений вскоре достигнет своего максимума, что приведет к отказу в обслуживании. Таким образом, атака зависит не от содержимого запроса, а от времени и характера отправляемых на сервер запросов.
Меры смягчения
- Используйте обратный прокси-сервер для приема и переадресации запросов в приложение Node.js. Обратные прокси-серверы могут обеспечивать кэширование, балансировку нагрузки, черный список IP-адресов и т. д., что снижает вероятность эффективности атаки типа «отказ в обслуживании».
- Правильно настройте тайм-ауты сервера, чтобы можно было отбрасывать неактивные соединения или соединения, в которых запросы поступают слишком медленно. См. различные тайм-ауты в
http.Server
, в частностиheadersTimeout
,requestTimeout
,timeout
иkeepAliveTimeout
. - Ограничьте количество открытых сокетов на хост и в целом. См. документацию по http, в частности
agent.maxSockets
,agent.maxTotalSockets
,agent.maxFreeSockets
иserver.maxRequestsPerSocket
.
Подмена DNS (CWE-346)
Это атака, которая может нацеливаться на приложения Node.js, работающие с включенным инспектором отладки с использованием ключа --inspect.
Поскольку веб-сайты, открытые в веб-браузере, могут отправлять запросы WebSocket и HTTP, они могут нацеливаться на инспектор отладки, работающий локально. Это обычно предотвращается политикой одного источника, реализованной в современных браузерах, которая запрещает скриптам доступ к ресурсам из разных источников (это означает, что вредоносный веб-сайт не может читать данные, запрошенные с локального IP-адреса).
Однако с помощью подмены DNS злоумышленник может временно контролировать источник своих запросов, чтобы они казались исходящими от локального IP-адреса. Это делается путем управления как веб-сайтом, так и DNS-сервером, используемым для разрешения его IP-адреса. Более подробную информацию см. в статье Википедии о подмене DNS.
Меры смягчения
- Отключите инспектор по сигналу SIGUSR1, прикрепив к нему прослушиватель
process.on(‘SIGUSR1’, … )
. - Не запускайте протокол инспектора в рабочей среде.
Раскрытие конфиденциальной информации неавторизованному субъекту (CWE-552)
Все файлы и папки, включенные в текущий каталог, отправляются в реестр npm во время публикации пакета.
Существуют некоторые механизмы для управления этим поведением путем определения черного списка с помощью .npmignore
и .gitignore
или путем определения белого списка в package.json
.
Меры по смягчению последствий
- Использование
npm publish --dry-run
для отображения списка всех файлов, предназначенных для публикации. Перед публикацией пакета обязательно проверьте содержимое. - Также важно создавать и поддерживать файлы игнорирования, такие как
.gitignore
и.npmignore
. В этих файлах можно указать, какие файлы/папки не следует публиковать. Свойство files вpackage.json
позволяет выполнить обратную операцию — создать разрешённый список. - В случае обнаружения уязвимости обязательно удаление пакета из публикации.
Контрабанда HTTP-запросов (CWE-444)
Это атака, которая включает в себя два HTTP-сервера (обычно прокси-сервер и приложение Node.js). Клиент отправляет HTTP-запрос, который сначала проходит через фронтенд-сервер (прокси-сервер), а затем перенаправляется на бэкенд-сервер (приложение). Когда фронтенд и бэкенд по-разному интерпретируют неоднозначные HTTP-запросы, существует вероятность того, что злоумышленник сможет отправить вредоносное сообщение, которое не будет видно фронтенду, но будет видно бэкенду, эффективно «пронося» его мимо прокси-сервера.
Более подробное описание и примеры см. в CWE-444.
Поскольку эта атака зависит от того, как Node.js интерпретирует HTTP-запросы, отличающиеся от (произвольного) HTTP-сервера, успешная атака может быть вызвана уязвимостью в Node.js, фронтенд-сервере или в обоих. Если способ интерпретации запроса Node.js согласуется со спецификацией HTTP (см. RFC7230), то это не считается уязвимостью в Node.js.
Меры по смягчению последствий
- Не используйте параметр
insecureHTTPParser
при создании HTTP-сервера. - Настройте фронтенд-сервер для нормализации неоднозначных запросов.
- Постоянно отслеживайте новые уязвимости контрабанды HTTP-запросов как в Node.js, так и в выбранном фронтенд-сервере.
- Используйте HTTP/2 от начала до конца и отключите понижение версии HTTP, если это возможно.
Раскрытие информации посредством атак на основе временных задержек (CWE-208)
Это атака, которая позволяет злоумышленнику получать потенциально конфиденциальную информацию, например, измеряя время отклика приложения на запрос. Эта атака не специфична для Node.js и может быть направлена практически на все среды выполнения.
Атака возможна всякий раз, когда приложение использует секрет в операции, чувствительной ко времени (например, ветвление). Рассмотрим обработку аутентификации в типичном приложении. Здесь базовый метод аутентификации включает электронную почту и пароль в качестве учетных данных. Информация о пользователе извлекается из введенных пользователем данных, предпочтительно из СУБД. После извлечения информации о пользователе пароль сравнивается с информацией о пользователе, полученной из базы данных. Использование встроенного сравнения строк занимает больше времени для значений одинаковой длины. Это сравнение, выполняемое в приемлемом количестве, непреднамеренно увеличивает время отклика запроса. Сравнивая время отклика запросов, злоумышленник может угадать длину и значение пароля за большое количество запросов.
Меры защиты
- API криптографии предоставляет функцию
timingSafeEqual
для сравнения фактических и ожидаемых конфиденциальных значений с использованием алгоритма с постоянным временем. - Для сравнения паролей можно использовать scrypt, также доступный в собственном криптомодуле.
- В более общем плане избегайте использования секретов в операциях с переменным временем. Это включает ветвление по секретам и, когда злоумышленник может находиться на той же инфраструктуре (например, на той же облачной машине), использование секрета в качестве индекса в памяти. Написание кода с постоянным временем в JavaScript сложно (отчасти из-за JIT). Для криптографических приложений используйте встроенные криптографические API или WebAssembly (для алгоритмов, не реализованных нативно).
Злонамеренные сторонние модули (CWE-1357)
В настоящее время в Node.js любой пакет может получать доступ к мощным ресурсам, таким как доступ к сети. Кроме того, поскольку они также имеют доступ к файловой системе, они могут отправлять любые данные куда угодно.
Весь код, работающий в процессе node, имеет возможность загружать и запускать дополнительный произвольный код с помощью eval()
(или его аналогов). Весь код с доступом на запись в файловую систему может добиться того же результата, записывая в новые или существующие файлы, которые загружаются.
Node.js имеет экспериментальный¹ механизм политик для объявления загруженного ресурса как ненадежного или надежного. Однако эта политика по умолчанию не включена. Обязательно указывайте версии зависимостей и запускайте автоматическую проверку уязвимостей с помощью распространенных рабочих процессов или npm-скриптов. Перед установкой пакета убедитесь, что этот пакет поддерживается и включает весь ожидаемый вами контент. Будьте осторожны, исходный код GitHub не всегда совпадает с опубликованным, проверьте его в node_modules
.
Атаки на цепочку поставок
Атака на цепочку поставок на приложение Node.js происходит, когда одна из его зависимостей (прямая или транзитивная) скомпрометирована. Это может произойти из-за того, что приложение слишком свободно указывает зависимости (позволяя нежелательные обновления) и/или из-за распространенных опечаток в спецификации (уязвимо для тайпосквоттинга).
Злоумышленник, получивший контроль над пакетом верхнего уровня, может опубликовать новую версию со вредоносным кодом. Если приложение Node.js зависит от этого пакета, не будучи строгим в отношении того, какая версия безопасна для использования, пакет может быть автоматически обновлен до последней вредоносной версии, что приведет к компрометации приложения.
Зависимости, указанные в файле package.json
, могут иметь точный номер версии или диапазон. Однако, при привязке зависимости к точному номеру версии, ее транзитивные зависимости сами по себе не привязаны. Это все еще оставляет приложение уязвимым для нежелательных/неожиданных обновлений.
Возможные векторы атаки:
- Атаки типа тайпосквоттинга
- Отравление lockfile
- Скомпрометированные сопровождающие
- Вредоносные пакеты
- Путаница зависимостей
Меры по смягчению
- Запретить npm выполнять произвольные скрипты с помощью
--ignore-scripts
- Кроме того, вы можете отключить его глобально с помощью
npm config set ignore-scripts true
- Кроме того, вы можете отключить его глобально с помощью
- Привяжите версии зависимостей к определенной неизменяемой версии, а не к версии, которая является диапазоном или из изменяемого источника.
- Используйте lockfiles, которые привязывают каждую зависимость (прямую и транзитивную).
- Используйте Меры по смягчению отравления lockfile.
- Автоматизируйте проверку на новые уязвимости с помощью CI, используя такие инструменты, как npm-audit.
- Такие инструменты, как
Socket
, могут использоваться для анализа пакетов с помощью статического анализа для поиска рискованного поведения, такого как доступ к сети или файловой системе.
- Такие инструменты, как
- Используйте
npm ci
вместоnpm install
. Это обеспечивает принудительное использование lockfile, так что несоответствия между ним и файломpackage.json
вызывают ошибку (вместо того, чтобы молча игнорировать lockfile в пользуpackage.json
). - Тщательно проверьте файл
package.json
на наличие ошибок/опечаток в именах зависимостей.
Нарушение доступа к памяти (CWE-284)
Атаки на основе памяти или кучи зависят от сочетания ошибок управления памятью и уязвимого выделения памяти. Как и все среды выполнения, Node.js уязвим для этих атак, если ваши проекты работают на общем компьютере. Использование защищенной кучи полезно для предотвращения утечки конфиденциальной информации из-за переполнения и недополнения указателей.
К сожалению, защищенная куча недоступна в Windows. Более подробную информацию можно найти в документации Node.js по защищенной куче.
Меры по смягчению
- Используйте
--secure-heap=n
в зависимости от вашего приложения, где n — выделенный максимальный размер в байтах. - Не запускайте свое производственное приложение на общем компьютере.
Monkey Patching (CWE-349)
Monkey patching относится к изменению свойств во время выполнения с целью изменения существующего поведения. Пример:
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
// переопределение глобального [].push
}
Меры по смягчению
Флаг --frozen-intrinsics
включает экспериментальные¹ замороженные встроенные объекты, что означает, что все встроенные объекты и функции JavaScript рекурсивно заморожены. Поэтому следующий фрагмент не переопределит поведение по умолчанию Array.prototype.push
// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
// переопределение глобального [].push
}
// Неперехваченное исключение:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Невозможно присвоить значение только для чтения свойству 'push' объекта '
Однако важно отметить, что вы все еще можете определять новые глобальные переменные и заменять существующие глобальные переменные с помощью globalThis
globalThis.foo = 3; foo; // вы все еще можете определять новые глобальные переменные 3
globalThis.Array = 4; Array; // однако вы также можете заменять существующие глобальные переменные 4
Поэтому Object.freeze(globalThis)
может использоваться для гарантии того, что глобальные переменные не будут заменены.
Атаки на основе загрязнения прототипов (CWE-1321)
Загрязнение прототипов относится к возможности изменения или внедрения свойств в элементы языка 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') // Неперехваченное исключение TypeError: d.hasOwnProperty — не функция
Это потенциальная уязвимость, унаследованная от языка JavaScript.
Примеры
- CVE-2022-21824 (Node.js)
- CVE-2018-3721 (Библиотека стороннего производителя: Lodash)
Меры по смягчению
- Избегайте небезопасного рекурсивного слияния, см. CVE-2018-16487.
- Реализуйте валидацию JSON Schema для внешних/недоверенных запросов.
- Создавайте объекты без прототипа, используя
Object.create(null)
. - Замораживание прототипа:
Object.freeze(MyObject.prototype)
. - Отключите свойство
Object.prototype.__proto__
, используя флаг--disable-proto
. - Проверяйте, существует ли свойство непосредственно в объекте, а не в прототипе, используя
Object.hasOwn(obj, keyFromObj)
. - Избегайте использования методов из
Object.prototype
.
Неконтролируемый элемент пути поиска (CWE-427)
Node.js загружает модули, следуя алгоритму разрешения модулей. Поэтому он предполагает, что каталог, в котором запрашивается модуль (require), является доверенным.
Это означает, что ожидается следующее поведение приложения. Предположим следующую структуру каталогов:
- app/
- server.js
- auth.js
- auth
Если server.js использует require('./auth')
, он будет следовать алгоритму разрешения модулей и загрузит auth вместо auth.js
.
Меры по смягчению
Использование экспериментального¹ механизма политик с проверкой целостности может предотвратить указанную выше угрозу. Для описанной выше директории можно использовать следующий policy.json
{
"resources": {
"./app/auth.js": {
"integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
},
"./app/server.js": {
"dependencies": {
"./auth": "./app/auth.js"
},
"integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
}
}
}
Таким образом, при требовании модуля auth система будет проверять целостность и выдаст ошибку, если она не соответствует ожидаемой.
» node --experimental-policy=policy.json app/server.js
node:internal/policy/sri:65
throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
^
SyntaxError [ERR_SRI_PARSE]: Строка целостности подресурса "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" имела неожиданный "%" в позиции 51
at new NodeError (node:internal/errors:393:5)
at Object.parse (node:internal/policy/sri:65:13)
at processEntry (node:internal/policy/manifest:581:38)
at Manifest.assertIntegrity (node:internal/policy/manifest:588:32)
at Module._compile (node:internal/modules/cjs/loader:1119:21)
at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
at Module.load (node:internal/modules/cjs/loader:1037:32)
at Module._load (node:internal/modules/cjs/loader:878:12)
at Module.require (node:internal/modules/cjs/loader:1061:19)
at require (node:internal/modules/cjs/helpers:99:18) {
code: 'ERR_SRI_PARSE'
}
Обратите внимание, всегда рекомендуется использовать --policy-integrity
, чтобы избежать изменений политики.
Экспериментальные функции в продакшене
Использование экспериментальных функций в продакшене не рекомендуется. Экспериментальные функции могут подвергаться критическим изменениям при необходимости, и их функциональность не является надежно стабильной. Тем не менее, обратная связь очень ценится.
Инструменты OpenSSF
OpenSSF возглавляет несколько инициатив, которые могут быть очень полезны, особенно если вы планируете опубликовать пакет npm. Эти инициативы включают в себя:
- OpenSSF Scorecard Scorecard оценивает проекты с открытым исходным кодом, используя ряд автоматизированных проверок на безопасность. Вы можете использовать его для проактивной оценки уязвимостей и зависимостей в вашей кодовой базе и принятия обоснованных решений о принятии уязвимостей.
- Программа значков лучших практик OpenSSF Проекты могут добровольно пройти самосертификацию, описав, как они соответствуют каждой лучшей практике. Это позволит сгенерировать значок, который можно добавить в проект.