Назначение

Этот документ призван расширить текущую модель угроз и предоставить подробные рекомендации по обеспечению безопасности приложения Node.js.

Содержание документа

  • Лучшие практики: Простой и сжатый способ ознакомиться с лучшими практиками. Мы можем использовать этот выпуск или данное руководство в качестве отправной точки. Важно отметить, что данный документ специфичен для Node.js, поэтому если вы ищете что-то более развернутое, рассмотрите OSSF Best Practices.

  • Объяснение атак: проиллюстрируйте и задокументируйте на понятном вам языке с примером кода (если возможно) атаки, которые мы упоминаем в модели угроз.

  • Библиотеки сторонних разработчиков: определение угроз (опечатки, вредоносные пакеты...) и лучшие практики в отношении зависимостей модулей node и т.д..

Список угроз

Отказ в обслуживании HTTP-сервера (CWE-400)

Это атака, при которой приложение становится недоступным для использования по назначению из-за того, как оно обрабатывает входящие HTTP-запросы. Эти запросы не обязательно должны быть намеренно составлены злоумышленником: неправильно сконфигурированный или работающий с ошибками клиент также может отправлять на сервер шаблон запросов, который приведет к отказу в обслуживании.

HTTP-запросы принимаются HTTP-сервером Node.js и передаются коду приложения через зарегистрированный обработчик запросов. Сервер не анализирует контент тела запроса. Поэтому любой DoS, вызванный содержимым тела запроса после его передачи обработчику, не является уязвимостью в самом Node.js, поскольку за правильную обработку отвечает код приложения.

Убедитесь, что веб-сервер правильно обрабатывает ошибки сокетов, например, если сервер был создан для работы без обработки ошибок, он будет уязвим для DoS.

const net = require('net');

const server = net.createServer(function(socket) {
  // socket.on('error', console.error) // this prevents the server to crash
  socket.write('Echo server\r\n');
  socket.pipe(socket);
});

server.listen(5000, '0.0.0.0');

Если будет выполнен неверный запрос сервер может выйти из строя.

Примером DoS-атаки, которая не вызвана содержимым запроса, является Slowloris. При такой атаке HTTP-запросы отправляются медленно и фрагментарно, по одному фрагменту за раз. Пока запрос не будет доставлен полностью, сервер будет удерживать ресурсы, выделенные для текущего запроса. Если одновременно послать достаточное количество таких запросов, то число параллельных соединений вскоре достигнет максимума, что приведет к отказу в обслуживании. Таким образом, атака зависит не от содержимого запроса, а от времени и характера посылаемых на сервер запросов.

Меры защиты (митигации уязвимостей):

  • Используйте обратный прокси-сервер для получения и перенаправления запросов к приложению Node.js. Обратные прокси могут обеспечивать кэширование, балансировку нагрузки, составление черных списков IP-адресов и т.д., что снизит эффективность DoS-атаки.

  • Правильно настройте таймауты сервера, чтобы соединения, которые простаивают или запросы, поступающие слишком медленно, могли быть прерваны. См. различные таймауты в http.Server, в частности headersTimeout, requestTimeout, timeout и keepAliveTimeout.

  • Ограничьте количество открытых сокетов на хосте и в целом. См. документацию по http, в частности agent.maxSockets, agent.maxTotalSockets, agent.maxFreeSockets и server.maxRequestsPerSocket.

Перепривязка (Rebinding) DNS (CWE-346)

Это атака, которая может быть направлена на приложения Node.js, запущенные с включенным инспектором отладки с помощью свитча --inspect.

Поскольку сайты, открытые в веб-браузере, могут выполнять WebSocket и HTTP запросы, то они способны выбрать в качестве мишени отладочный инспектор, запущенный локально. Обычно этому препятствует политика same-origin, применяемая в современных браузерах, которая запрещает скриптам обращаться к ресурсам разного происхождения (это означает, что вредоносный веб-сайт не сможет прочитать данные, запрошенные с локального IP-адреса).

Однако с помощью перепривязки DNS злоумышленник может временно управлять происхождением своих запросов так, чтобы казалось, что они исходят с локального IP-адреса. Для этого необходимо контролировать как веб-сайт, так и DNS-сервер, используемый для разрешения его IP-адреса. Более подробную информацию см. в разделе DNS Rebinding wiki.

Митигации:

  • Отключите инспектор по сигналу 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-downgrading (понижение).

Раскрытие информации в результате атак по времени (CWE-208)

Это атака, которая позволяет злоумышленнику узнать потенциально важную информацию, например, измерив, сколько времени требуется приложению для ответа на запрос. Эта атака не является специфичной для Node.js и может быть направлена практически на все рантаймы.

Атака возможна всякий раз, когда приложение использует секрет в чувствительной к времени операции (например, ветвление). Рассмотрим обработку аутентификации в типовом приложении. Здесь базовый метод аутентификации включает в себя электронную почту и пароль в качестве учетных данных. Информация о пользователе извлекается из входных данных, предоставленных пользователем, в идеале из СУБД. После получения информации о пользователе пароль сравнивается с его данными, полученными из базы данных. Использование встроенного сравнения строк занимает больше времени при одинаковой длине значений. Такое сравнение, если оно выполняется в течение определенного времени, непроизвольно увеличивает время отклика запроса. Сравнивая время отклика запроса, злоумышленник может определить длину и значение пароля в большом количестве запросов.

Митигации:

  • Криптографический API предоставляет функцию timingSafeEqual для сравнения фактического и ожидаемого значений чувствительности с использованием алгоритма постоянного времени (выполняемый за постоянное/константное время).

  • При сравнении паролей можно использовать scrypt, доступный также в нативном криптомодуле.

  • В целом, избегайте использования секретов в операциях с изменяемым временем. Это включает в себя ветвление на секретах и, когда атакующий может находиться в одной инфраструктуре (например, на одной облачной машине), использование секрета в качестве индекса в памяти. Писать код постоянного времени в JavaScript сложно (отчасти из-за JIT). Для криптографических приложений используйте встроенные криптографические API или WebAssembly (для алгоритмов, не реализованных нативно).

Вредоносные модули сторонних разработчиков (CWE-1357)

В настоящее время в Node.js любой пакет может получить доступ к мощным ресурсам, таким как выход в сеть. Более того, поскольку они также имеют доступ к файловой системе, то могут отправлять любые данные куда угодно.

Весь код, запущенный в процессе узла, имеет возможность загружать и выполнять дополнительный произвольный код с помощью eval() (или его эквивалентов). Код с возможностью записи в файловую систему может добиться того же, занося данные в новые или существующие файлы, которые уже загружены.

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

Атаки на цепочку поставок

Атака цепочки поставок приложения Node.js происходит, когда одна из его зависимостей (прямая или транзитивная) оказывается под угрозой. Это может произойти либо из-за слишком слабой спецификации зависимостей в приложении (что допускает нежелательные обновления), либо из-за обычных опечаток в спецификации (уязвимость к тайпсквоттингу).

Злоумышленник, получивший контроль над upstream-пакетом, может опубликовать новую версию с вредоносным кодом. Если приложение Node.js зависит от этого пакета, не имея строгих требований к тому, какую версию безопасно использовать, то он может быть автоматически обновлен до последней вредоносной версии, что скомпрометирует приложение.

Зависимости, указанные в файле package.json, могут иметь точный номер версии или их диапазон. Однако в случае, если запинить зависимость к точной версии, ее транзитивные зависимости сами по себе не зафиксируются. Это по-прежнему оставляет приложение уязвимым к нежелательным/непредусмотренным обновлениям.

Возможные векторы атак:

  • Атаки с использованием тайпсквоттинга

  • Заражение файла блокировки

  • Скомпрометированные мэйнтейнеры

  • Вредоносные пакеты

  • Запутанные зависимости

Митигации:

  • Запретите npm выполнять произвольные скрипты с помощью опции --ignore-scripts

  • Кроме того, вы можете отключить его глобально с помощью npm config set ignore-scripts true

  • Запинивайте версии зависимостей к конкретной неизменяемой версии, а не к версии, которая является частью диапазона или получена из изменяемого источника.

  • Используйте lockfiles (файлы блокировки), которые запинивают каждую зависимость (прямую и транзитивную).

  • Используйте средства защиты от заражения файлов блокировки.

  • Автоматизируйте проверки на новые уязвимости с помощью 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 - максимальный размер аллоцированного байта.

  • Не запускайте свое рабочее приложение на общей машине.

Манкипатчинг (CWE-349)

Под "манкипатчингом" понимается модификация свойств в процессе рантайма с целью изменения существующего поведения. Пример:

// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // overriding the global [].push
};

Митигации

Флаг --frozen-intrinsics включает экспериментальные¹ замороженные интринсики. Это означает, что все встроенные объекты и функции JavaScript рекурсивно заморожены. Поэтому представленный ниже сниппет не отменяет поведение по умолчанию Array.prototype.push

// eslint-disable-next-line no-extend-native
Array.prototype.push = function (item) {
  // overriding the global [].push
};

// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object ''

Однако важно отметить, что вы все еще можете определять новые и заменять существующие глобальные переменные с помощью globalThis

> globalThis.foo = 3; foo; // you can still define new globals
3
> globalThis.Array = 4; Array; // However, you can also replace existing globals
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

// Potential 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-2018-16487.

  • Реализуйте проверку схемы JSON для внешних/недоверенных запросов.

  • Создавайте объекты без прототипа с помощью Object.create(null).

  • Замораживайте прототип: Object.freeze(MyObject.prototype).

  • Отключите свойство Object.prototype.__proto__ с помощью флага --disable-proto.

  • Проверьте, что свойство существует непосредственно у объекта, а не у прототипа, используя Object.hasOwn(obj, keyFromObj).

  • Избегайте использования методов из Object.prototype.

Элемент неконтролируемого поискового пути (CWE-427)

Node.js загружает модули, следуя алгоритму разрешения модулей. Поэтому он предполагает, что каталог, из которого запрашивается (требуется) модуль, является доверенным.

Это означает, что ожидается следующее поведение приложения. Предполагается такая структура каталога:

  • 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]: Subresource Integrity string "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8=%" had an unexpected "%" at position 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, чтобы не допустить мутаций политики.

1Экспериментальные фичи в продакшне

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

В заключение приглашаю всех на бесплатный урок, где мы рассмотрим протокол GRPC, технические характеристики и области применения. Мы научимся создавать простейшие GRPC клиенты и серверы на Node.js, а также микросервисы с NestJS. Это занятие будет полезно тем, кто хочет познакомиться со стандартом GRPC, сравнить этот подход с традиционным REST API и использовать его в среде Node.js

Также напоминаем о том, что 22 декабря пройдет бесплатный вебинар курса Vue.js по теме: "Основы Nuxt 3", узнать подробнее о котором можно тут.

Комментарии (0)