Skip to content

Рекомендации по безопасности

Цель

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

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

  • Рекомендации: упрощенный и сжатый способ ознакомления с лучшими практиками. Мы можем использовать этот вопрос или это руководство в качестве отправной точки. Важно отметить, что этот документ относится конкретно к Node.js, если вы ищете что-то более широкое, рассмотрите лучшие практики OSSF.
  • Объяснение атак: иллюстрация и документирование простым языком с некоторыми примерами кода (если это возможно) атак, которые мы упоминаем в модели угроз.
  • Сторонние библиотеки: определение угроз (атаки с опечатками, вредоносные пакеты...) и лучшие практики в отношении зависимостей модулей node и т. д.

Список угроз

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

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

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

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

javascript
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-адресов и т. д., что снижает вероятность успешной DoS-атаки.
  • Правильно настроить тайм-ауты сервера, чтобы соединения, которые неактивны или запросы которых поступают слишком медленно, могли быть разорваны. См. различные тайм-ауты в http.Server, особенно headersTimeout, requestTimeout, timeout и keepAliveTimeout.
  • Ограничить количество открытых сокетов на хост и общее количество. См. документацию по http, особенно agent.maxSockets, agent.maxTotalSockets, agent.maxFreeSockets и server.maxRequestsPerSocket.

DNS Rebinding (CWE-346)

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

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

Однако с помощью DNS rebinding злоумышленник может временно контролировать источник своих запросов, чтобы они, казалось, исходили с локального 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, если это возможно.

Information Exposure through Timing Attacks (CWE-208)

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

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

Mitigations

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

Malicious Third-Party Modules (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, которые закрепляют каждую зависимость (прямую и транзитивную).
  • Автоматизируйте проверки на наличие новых уязвимостей с помощью 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 относится к изменению свойств во время выполнения с целью изменения существующего поведения. Пример:

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

Меры по смягчению последствий

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

js
// 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

bash
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 и других свойств, унаследованных от встроенных прототипов.

js
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 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

json
{
  "resources": {
    "./app/auth.js": {
      "integrity": "sha256-iuGZ6SFVFpMuHUcJciQTIKpIyaQVigMZlvg9Lx66HV8="
    },
    "./app/server.js": {
      "dependencies": {
        "./auth": "./app/auth.js"
      },
      "integrity": "sha256-NPtLCQ0ntPPWgfVEgX46ryTNpdvTWdQPoZO3kHo0bKI="
    }
  }
}

Следовательно, при запросе модуля auth система будет проверять целостность и выдавать ошибку, если она не соответствует ожидаемой.

bash
» 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, чтобы избежать мутаций политики.

Экспериментальные функции в Production

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

Инструменты OpenSSF

OpenSSF возглавляет несколько инициатив, которые могут быть очень полезными, особенно если вы планируете опубликовать npm-пакет. Эти инициативы включают:

  • OpenSSF Scorecard Scorecard оценивает проекты с открытым исходным кодом, используя серию автоматизированных проверок рисков безопасности. Вы можете использовать его для проактивной оценки уязвимостей и зависимостей в вашей кодовой базе и принятия обоснованных решений о принятии уязвимостей.
  • OpenSSF Best Practices Badge Program Проекты могут добровольно самосертифицироваться, описывая, как они соответствуют каждой передовой практике. Это сгенерирует значок, который можно добавить в проект.