npm audit работает неправильно — это настоящий театр безопасности

Безопасность важна. Никто не хочет выступать против безопасности. Поэтому все замалчивают проблему с npm audit. Но кто-то должен сказать.

Думаю, придётся мне.

npm audit работает принципиально неправильно. Проверка по умолчанию на каждый npm install — поспешный, непродуманный и неадекватный подход.

Слышали историю про мальчика, который часто кричал «Волк!»? Спойлер: в результате волк съел овец. Если мы не хотим такого итога, нам нужны лучшие инструменты.

На сегодняшний день npm audit — это пятно на всей экосистеме npm. Надо было исправить его с самого начала, но лучше поздно, чем никогда.

Примечание: статья написана в критическом и несколько язвительном тоне. Понимаю, что очень сложно поддерживать такие масштабные проекты, как Node.js/npm, и ошибки очевидны не сразу. Меня расстраивает только сама ситуация, а не люди. Я сохранил язвительный тон, потому что моё разочарование росло с каждым годом. И я не хочу притворяться, что ситуация лучше, чем она есть на самом деле. Печальнее всего смотреть на ребят, у которых это первый опыт программирования. И на тех, кто не может задеплоить свои проекты из-за нерелевантных предупреждений npm audit. Я рад, что эта проблема обсуждается, и сделаю всё возможное, чтобы внести свой вклад в предлагаемые решения!

Как работает npm audit?

Промотайте эту часть, если вы с ней знакомы.

У приложения Node.js есть дерево зависимостей. Оно может выглядеть следующим образом:

your-app
  - [email protected]
  - [email protected]
  - [email protected]
    - [email protected]
      - [email protected]

Скорее всего, дерево гораздо глубже.

Теперь предположим, что обнаружена уязвимость в [email protected]:

your-app
  - [email protected]
  - [email protected]
  - [email protected]
    - [email protected]
      - [email protected] (Vulnerable!)

Эти данные публикуются в специальном реестре, к которому npm получит доступ при следующем запуске npm audit. Начиная с шестой версии, после каждой установки зависимостей npm install выводится сообщение:

1 vulnerabilities (0 moderate, 1 high)

To address issues that do not require attention, run:
npm audit fix

To address all issues (including breaking changes), run:
npm audit fix --force

Выполняете npm audit fix, и npm пытается установить последнюю версию [email protected] с исправлениями. Если database-layer указывает, что зависит не точно от [email protected], а от некоторого допустимого диапазона, включая 1.0.1, исправление просто работает:

your-app
  - [email protected]
  - [email protected]
  - [email protected]
    - [email protected]
      - [email protected] (Fixed!)

Но возможно, что [email protected] строго зависит от [email protected]. В этом случае мейнтейнер database-layer должен выпустить новую версию, которая позволит использовать [email protected]:

your-app
  - [email protected]
  - [email protected]
  - [email protected]
    - [email protected] (Updated to allow the fix.)
      - [email protected] (Fixed!)

Наконец, если нет возможности изящно обновить дерево, можно попробовать npm audit fix --force. Это на случай, если database-layer не принимает новую версию network-utility, а также не апдейтится. Здесь вы как бы берёте на себя потенциальные риски. Кажется, что это разумный вариант.

Вот как должен работать npm audit в теории.

Как сказал кто-то мудрый, в теории нет разницы между теорией и практикой. Но разница есть на практике. И вот тут начинается самое интересное.

Почему это неправильный подход

Посмотрим, что происходит на практике. Для тестирования возьмём Create React App. Этот набор включает множество инструментов, в том числе Babel, webpack, TypeScript, ESLint, PostCSS, Terser и другие. Create React App берёт ваш исходный код JavaScript — и преобразует в статичную папку HTML+JS+CSS. Примечательно, что он не создаёт приложение Node.js.

Итак, делаем новый проект!

npx create-react-app myapp

Сразу после создания проекта видим следующее:

found 5 vulnerabilities (3 moderate, 2 high)
run `npm audit fix` to fix them, or `npm audit` for details

Чёрт побери! В только что созданном приложении уже уязвимости!

Так говорит npm.

Запустим npm audit и посмотрим, что случилось.

Первая «уязвимость»

Вот первая проблема, о которой сообщает npm audit:

┌───────────────┬──────────────────────────────────────────────────────────────┐
│ Moderate │ Regular Expression Denial of Service │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ browserslist │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in │ >=4.16.5 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ react-scripts │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ react-scripts > react-dev-utils > browserslist │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://npmjs.com/advisories/1747 │
└───────────────┴──────────────────────────────────────────────────────────────┘

Очевидно, что уязвим browserslist. Что это такое и как используется? Create React App генерирует файлы CSS, оптимизированные для конкретного списка браузеров. Например, в файле package.json можно указать, что вы ориентируетесь только на современные браузеры:

 "browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}

Тогда он уберёт из выдачи устаревшие хаки flexbox. Поскольку множество инструментов полагаются на один и тот же формат конфигурации целевых браузеров, Create React App использует общий пакет browserslist для парсинга файла конфигурации.

Так в чём уязвимость? «Отказ в обслуживании по регулярному выражению» означает, что злоумышленник может создать специальную строку конфигурации, способную экспоненциально замедлить browserslist. Звучит пугающе…

Погодите, что?! Давайте вспомним, как работает приложение. На вашей машине есть файл конфигурации. Вы создаёте проект. Получаете статический HTML+CSS+JS в папке. Размещаете его на статическом хостинге. У пользователя просто нет возможности повлиять на конфигурацию package.json. Это не имеет никакого смысла. Если злоумышленник уже имеет доступ к вашей машине и может изменять ваши конфигурационные файлы, у вас гораздо более серьёзная проблема, чем медленные регулярные выражения!

Итак, я полагаю, что эта «умеренная» «уязвимость» не является ни умеренной, ни уязвимостью в контексте проекта. Проехали.

Вердикт: эта «уязвимость» абсурдна в данном контексте.

Вторая «уязвимость»

Вот следующая проблема, о которой с готовностью сообщил npm audit:

┌───────────────┬──────────────────────────────────────────────────────────────┐
│ Moderate │ Regular expression denial of service │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ glob-parent │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in │ >=5.1.2 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ react-scripts │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ react-scripts > webpack-dev-server > chokidar > glob-parent │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://npmjs.com/advisories/1751 │
└───────────────┴──────────────────────────────────────────────────────────────┘

Рассмотрим цепочку зависимостей webpack-dev-server > chokidar > glob-parent. Здесь webpack-dev-server — сервер только для разработки, для быстрого локального обслуживания вашего приложения. Он использует chokidar для наблюдения за изменениями в файловой системе (например, когда вы сохраняете файл в редакторе). И glob-parent для извлечения части пути к файловой системе из шаблона файл-вотчера.

К сожалению, glob-parent уязвим! Если злоумышленник предоставит специально созданный путь к файлу, он может сделать эту функцию экспоненциально медленной, что приведёт к…

Погодите, что?! Сервер разработки находится на вашем компьютере. Файлы находятся на вашем компьютере. Файл-вотчер использует указанную вами конфигурацию. Ни одна из этих конструкций не покидает ваш компьютер. Если злоумышленник достаточно изощрён, чтобы войти на вашу машину во время локальной разработки, последнее, что он захочет сделать, это создать специальные длинные пути к файлам, чтобы замедлить вашу разработку. Так что эта угроза просто абсурдна.

Похоже, что эта «умеренная» «уязвимость» не является ни умеренной, ни уязвимостью в контексте проекта.

Вердикт: эта «уязвимость» абсурдна в данном контексте.

Третья «уязвимость»

Давайте посмотрим на это:

┌───────────────┬──────────────────────────────────────────────────────────────┐
│ Moderate │ Regular expression denial of service │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ glob-parent │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in │ >=5.1.2 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ react-scripts │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ react-scripts > webpack > watchpack > watchpack-chokidar2 > │
│ │ chokidar > glob-parent │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://npmjs.com/advisories/1751 │
└───────────────┴──────────────────────────────────────────────────────────────┘

Подождите, это то же самое, что и выше, но через другой путь зависимости.

Вердикт: эта «уязвимость» абсурдна в данном контексте.

Четвёртая «уязвимость»

Уф, выглядит очень плохо! npm audit осмелился показать её красным цветом:

┌───────────────┬──────────────────────────────────────────────────────────────┐
│ High │ Denial of Service │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ css-what │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in │ >=5.0.1 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ react-scripts │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ react-scripts > @svgr/webpack > @svgr/plugin-svgo > svgo > │
│ │ css-select > css-what │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://npmjs.com/advisories/1754 │
└───────────────┴──────────────────────────────────────────────────────────────┘

Что это за проблема «высокой» важности? «Отказ в обслуживании»? Я не хочу, чтобы в обслуживании было отказано! Это будет очень плохо… Если только…

Давайте посмотрим внимательнее на проблему. Очевидно, что парсер для CSS-селекторов css-what может замедлиться при получении специально созданных входных данных. Этот парсер используется плагином, который генерирует компоненты React из SVG-файлов.

Это означает, что если злоумышленник получит контроль над моей машиной разработки или моим исходным кодом, он поместит специальный SVG-файл, в котором будет находиться специально созданный CSS-селектор, что сделает мою сборку медленной. Уязвимость проверена и подтверждена…

Подождите, что?! Если злоумышленник может изменить исходный код моего приложения, то просто добавит туда криптомайнер. Зачем добавлять файлы SVG, если только они не майнят монеро? Опять же, в этом никакого смысла.

Вердикт: «уязвимость» абсурдна в данном контексте.

Вот вам и «высокая» важность.

Пятая «уязвимость»

┌───────────────┬──────────────────────────────────────────────────────────────┐
│ High │ Denial of Service │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ css-what │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in │ >=5.0.1 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ react-scripts │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ react-scripts > optimize-css-assets-webpack-plugin > cssnano │
│ │ > cssnano-preset-default > postcss-svgo > svgo > css-select │
│ │ > css-what │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://npmjs.com/advisories/1754 │

└───────────────┴──────────────────────────────────────────────────────────────┘

То же самое…

Вердикт: эта «уязвимость» абсурдна в данном контексте.

Продолжать?

Мальчик уже пять раз прокричал «Волк!». Два предупреждения — это дубли. Остальные абсурдны и не имеют значения в контексте использования этих зависимостей.

Пять ложноположительных срабатываний — не так уж плохо.

К сожалению, их сотни.

Вот несколько типичных тредов, есть и много других:

Я потратил несколько часов, но изучил абсолютно все проблемы, о которых сообщил npm audit за последние несколько месяцев, и похоже, что все они являются ложными срабатываниями в контексте такого инструмента для сборки, как Create React App, в виде «набора зависимостей».

Конечно, их можно исправить. Можно ослабить некоторые зависимости верхнего уровня, расширив «фокус» вместо указания конкретных версий (что приведёт к более частому проскальзыванию багов в патчи). Можно участить релизы, чтобы этот театр безопасности никогда нас не догнал.

Но этого недостаточно. Представьте, если б ваши тесты в 99% случаев не срабатывали по надуманным причинам! Это бесполезная трата многих человеко-лет и совершенно лишние проблемы. Причём страдают все категории:

  • Страдают новички, потому что для них это первое впечатление от программирования в экосистеме Node.js. Как будто установка Node.js/npm недостаточно запутана сама по себе (не повезло, если вы где-то добавили sudo, потому что так написано в учебнике). А теперь они сталкиваются с повсеместными «уязвимостями», когда пробуют онлайн-примеры или создают проект. Новичок смутно знаком с RegExp. Он никак не может понимать, насколько серьёзны уязвимости RegExp DDoS или загрязнение прототипа при использовании инструмента сборки для создания статического HTML+CSS+JS.
  • Страдают опытные разработчики: им приходится тратить время на явно ненужную работу или бороться со своими отделами безопасности, пытаясь объяснить, что npm audit — это сломанный инструмент, который изначально не подходит для реального аудита безопасности. Да, по какой-то причине сломанный инструмент используется по умолчанию.
  • Страдают мейнтейнеры: вместо того, чтобы работать над исправлениями и улучшениями, им приходится принимать исправления фиктивных багов. Иначе пользователи будут расстроены и напуганы.
  • Когда-нибудь пострадают и сами пользователи, потому что мы обучили целое поколение разработчиков либо не уделять внимания предупреждениям безопасности, либо просто игнорировать их. Оно и понятно: эти предупреждения всё появляются и появляются, и каждый раз опытные разработчики (правильно) говорят, что реальной проблемы нет.

Не помогают и глюки npm audit fix. Сегодня я запустил npm audit fix --force, и он понизил основную зависимость до версии трёхлетней давности с реальными уязвимостями. Спасибо, npm, отличная работа.

Что делать?

Я не знаю, как решить проблему. Не я её создал, так что не мне решать. Знаю только то, что нынешняя система не работает.

Есть несколько возможных решений.

  • Переместить зависимость в devDependencies, если она не работает в продакшне. Предполагается, что такие dev-зависимости не несут риска. Однако решение несовершенно:
    • npm audit по умолчанию всё равно предупреждает о наличии dev-зависимостей. Чтобы скрыть их, нужно знать о существовании npm audit --production. Люди, которые это знают, уже не доверяют инструменту в любом случае. Это также не поможет новичкам или сотрудникам компаний с параноидальным отделом безопасности.
    • npm installпо-прежнему использует информацию из обычного npm audit, поэтому при любой установке вы будете видеть все ложные срабатывания.
    • Как скажет любой специалист по безопасности, dev-зависимости на самом деле представляют вектор атаки, и, возможно, одним из самых опасных, потому что их так трудно обнаружить, а код работает с высоким доверием. Вот почему ситуация настолько плоха: любая реальная проблема погребена под десятками не относящихся к делу вопросов, которые программисты и мейнтейнеры привыкают игнорировать. Лишь вопрос времени, когда такая атака произойдёт на самом деле.
  • Встраивание всех зависимостей при публикации. Так всё чаще делают пакеты вроде Create React App. Например, Vite и Next.js просто собирают свои зависимости непосредственно в пакет, а не полагаются на механизм npm node_modules. С точки зрения мейнтейнера, плюсы очевидны: вы получаете более быстрое время загрузки, меньший объём загружаемых файлов и — в качестве приятного бонуса — отсутствие ложных сообщений об уязвимостях от ваших пользователей. Это изящный способ обыграть систему. Но меня беспокоит, какие стимулы создаются для экосистемы. Встраивание зависимостей как бы противоречит всему смыслу npm.
  • Какой-нибудь способ «апелляций» для уязвимостей. Конечно, эта проблема известна Node.js и npm. Разные люди работали над различными предложениями по её устранению. Например, есть предложение реализовать способ ручного устранения предупреждений, чтобы они не появлялись снова. Однако это всё равно возлагает бремя на пользователей, которые не всегда имеют представление о том, что за уязвимости скрываются глубже в дереве — настоящие или ложные. У меня также есть предложение: реализовать механизм, чтобы разработчик мог передать пользователям информацию, что определённая уязвимость не может на них повлиять. Если вы запускаете мой код на своём компьютере, то вы же доверяете моим суждениям? Буду рад обсудить и другие варианты.

Корень проблемы в том, что npm добавил поведение по умолчанию, которое во многих ситуациях приводит к 99% и более ложных срабатываний, путает новичков, разжигает конфликты с отделами безопасности, подталкивает мейнтейнеров навсегда уйти из экосистемы Node.js, а в какой-то момент приведёт к тому, что действительно опасные уязвимости пройдут незамеченными.

Нужно что-то делать.

Тем временем я собираюсь закрывать на GitHub все задачи, связанные с npm audit, которые не соответствуют реальным уязвимостям, способным повлиять на проект. Предлагаю другим мейнтейнерам принять такую же политику. Это вызовет разочарование у пользователей, но суть проблемы заключается в npm. Мне надоел этот театр безопасности. У Node.js/npm есть все возможности для решения проблемы. Я с ними на связи и надеюсь, что эта проблема станет приоритетной.

Сейчас npm audit сломан.

Новички, опытные разработчики, мейнтейнеры, отделы безопасности и, самое главное, наши пользователи — заслуживают лучшего.



Источник 📢