14 December 2017

Зависимости наших зависимостей или несколько слов об уязвимости наших проектов

Website developmentJavaScriptProgrammingNode.JS

Зависимости наших зависимостей



Эта история началась 30 ноября, утром. Когда вполне обычный билд на Test environment внезапно упал. Наверное, какой-то линтер отвалился, не проснувшись подумал я и был не прав.


Кому интересно чем закончилась эта история и на какие мысли навела – прошу под кат.


Открыв билд-лог, я увидел, что упал npm install. Странно подумал, я. Еще вчера вечером все работало отлично. После некоторого изучения логов, была найдена подозрительная строка:


это подозрительно...
node --eval 'if (require("./package.json").name === "coffee-script") { var red, yellow, cyan, reset; red = yellow = cyan = reset = ""; if (!process.env.NODE_DISABLE_COLORS) { red = "\x1b[31m"; yellow = "\x1b[33m"; cyan = "\x1b[36m"; reset = "\x1b[0m"; } console.warn(red + "CoffeeScript has moved!" + reset + " Please update references to " + yellow + "\"coffee-script\"" + reset + " to use " + yellow + "\"coffeescript\"" + reset + " (no hyphen) instead."); console.warn("Also, a new major version has been released under the " + yellow + "coffeescript" + reset + " name on NPM. This new release targets modern JavaScript, with minimal breaking changes. Learn more at " + cyan + "http://coffeescript.org" + reset + "."); console.warn(""); }

Здесь я опять удивился. Опять же, еще вчера мы coffee-script на проекте не использовали и за ночь вряд ли что-то сильно изменилось. Быстрый просмотр package.json подтвердил, что никакой супостат ничего нового туда не добавлял. Значит, наверное, у нас обновилась какая-то зависимость, которая использует coffee-script. Но против этой идеи говорило то, что уже довольно давно я выставил строгие версии для всех зависимостей проекта и, как мне казалось, такого случиться не могло. Бесплодно поискав похожую проблему в интернете, я опять вернулся к мысли об обновившейся зависимости. Поэтому на коленке был набросан скрипт который обошел все package.json-ы в node_modules в поисках coffee-script. Таких зависимостей оказалось около 5-6 штук. Это еще больше укрепило мои подозрения, и не сильно долго размышляя я снес весь node_modules, а заодно и все dependencies кроме одной в локальном репозитории и запустил npm install снова. Процесс прошел успешно. Дальше, шаг за шагом была найдена та зависимость, которая и валила install.


Это оказалась karma-typescript у которой в транзитивной зависимости оказался "pad", который в свою очередь зависел от coffee-script. И тут я снова приуныл. Вариантов было немного. Или временно отключать тесты, или ждать фикса, или делать форк и чинить самому (причем не очень понятно, что же конкретно нужно чинить). Без особой надежды я отправился на Github создавать issue. Каково же было мое удивление, когда мне ответили буквально через 20 минут. Оказалось, что некоторый товарищ, решил обновить coffee-script пакет в npm и вместо того, чтобы объявить старый пакет устаревшим он просто сделал ему unpublish.


один из комментариев от сообщества:

На мое счастье, пользователь @ondrejbase уже предложил костыль временное решение которое мне помогло. И вся эта история закончилась относительно дешево. Но могло быть совсем не так.


Это была присказка. А теперь я предлагаю поговорить о зависимостях наших проектов и проблемах, которые они нам приносят.


Давайте начнём с простого.


Глобальные зависимости


Недавно я в очередной раз наткнулся на статью, для новичков которая начиналась со строки


npm install -g typescript


И не выдержал. По моему мнению это один из самых плохих советов который вы можете дать начинающему разработчику. Я серьезно. Вот проблемы, к которым приводит этот совет:


  • Конфликт локальных репозиториев. Собирая весь код с помощью глобальных зависимостей (tsc, например) или валидируя его глобальным линтерами мы уже не можем так легко обновлять эти зависимости. Ведь обновив что-то глобальное, мы ставим под угрозу все репозитории в которых это используется. В итоге, придется или тратить время на то что бы обновить эти зависимости во всех репозиториях, или не обновлять их совсем.
  • Конфликт с членами команды. Использование глобальных зависимостей в командной работе приводит к их рассинхронизации. И, как следствие, к тому, что мой код не соберется или не пройдет проверку у моего коллеги.
  • Конфликт с билд-машиной. То, что соберется у меня, не гарантировано соберется на билд машине. Или, что гораздо хуже и коварнее — соберется не так, как у меня. А это гарантированный ад дебага на ближайший час или больше.

И все это происходит потому, большинство пособий начинается с npm install abc -g. Хотя можно поставить все локально и подключить в package.json как ./node_modules/.bin/tsc


Если вы соберетесь писать свой гайд, пожалуйста, вспомните эту статью и спасите мои нервы, а также нервы всех тех, кто будет ее использовать на практике.


N.B. Глобально устанавливать генераторы кода (create-react-app, create-angular-app) это нормально. Они один раз отработают и все. Кроме того, вам не понадобится ставить их заново, когда вы решите создать следующий репозиторий.


Нестрогие зависимости


Давайте идти дальше. Установим create-react-app, и создадим базовое приложение. Заходим в package.json и что мы там видим?


"react": "^16.2.0"


Все (или почти все) знают, что значит символ ^. Он значит, что npm может установить любую версию старше или равной указанной, в пределах мажорного релиза. И что в этом плохого? Не хочется быть категоричным, но, как по мне этот подход тоже "не очень", и вот почему:


  • Потеря контроля. Как только вы оставили ^ или ~ в своем package.json вы потеряли контроль над своим кодом. Конечно я утрирую, но подумайте. Вы, грузите в проект версию сторонней библиотеки о которой не знаете даже ее версии. Только то, что она лежит в репозитории от какого-то издателя. И вдруг что случится, остается только надеяться, что у этого пакета достаточно организованное сообщество что бы это заметить.
  • Изменения ломающие обратную совместимость. Конечно, публикуя мажорную версию, разработчики обещают, что в ее рамках breaking changes не будет. Но, господа, давайте быть реалистами. Это интернет, и это open source: вам никто ничего не должен. То, что работало час назад, может отвалиться с комментарием типа: “This new release targets modern JavaScript, with minimal breaking changes”.
  • Неочевидность обновлений. Допустим есть у меня библиотека вида abc: ^2.1.1. И, получается, я не знаю какая на самом деле версия библиотеки у меня стоит. Может быть 2.1.1, а может быть 2.9.9. И вроде бы это не проблема, наоборот. Мне не нужно искать документацию для определенной версии, достаточно всегда смотреть самую свежую. И вот я ее смотрю, и вижу новую фишку, и она не работает. А не работает она потому что я просто забыл обновить библиотеку. Я ее обновляю, комичу код, и через 30 минут ко мне прибегает мой коллега потому что у него приложение не работает! А за ним приходят еще трое и делают мне грустно. Кому это нужно?
  • И наконец: зависимости зависимостей. Это вишенка на торте и это то, о чем я бы хотел, чтобы вы задумались.

Зависимости наших зависимостей или ЗНЗ


Давайте начнем с того, почему у нас сломался билд. Все зависимости были заданы жестко и тем не менее мы свалились. Несмотря на то, что версии наших основных зависимостей оставались прежними (напомню, никаких ^, ~ в package.json) их зависимости были не такими строгими. Мы не контролировали зависимости наших зависимостей, хотя и пытались. И, что самое неприятное такое поведение поощряется по умолчанию. Я не знаю кто и зачем это сделал, но он подложил большую свинью всем нам, а особенно тем, кто практикует continuous-integration.


Конечно, конкретно эту проблему исправить легко. Достаточно создать lock-file (например, с помощью команды npm shrinkwrap) или использовать yarn — пакетный менеджер который по умолчанию фиксирует все зависимости вашего проекта.


Однако это решает только часть проблемы. Остается еще одна, гораздо более опасная ее часть и имя ей unpublish. Если вы не сталкивались с этой проблемой до этого, то вот тут прекрасная статья которая показывает всю уязвимость современной веб разработки. В любой момент, умышленно или по неосторожности, ваш проект может перестать собираться только потому, что кто-то удалил свой пакет из npm. И сделать это отнюдь не сложно. Достаточно просто ввести команду unpublish. С этой бедой можно бороться. Но давайте будем честны перед собой? У кого из нас стоит свой локальный npm? А кто хотя бы задумывался об этом? Боюсь, что не так много. И я лишь надеюсь, что теперь вы предупреждены.
Кстати, если вы зайдете в мой репозиторий (не делайте этого, прошу), то увидите, что почти все мои проекты нарушают все, что было сказано выше. И это еще одно доказательство того, насколько проблема распространена.


Выводы


  • Используйте флаг глобальной установки с умом. Не стоит использовать его для тех зависимостей от которых будет постоянно зависеть ваш проект.
  • Если вы работаете не один или на нескольких физических машинах, подумайте о том, чтобы задать зависимости строго. Это позволит вам иметь одинаковые основные зависимости проекта во всех репозиториях и у коллег. Кроме того, вы увеличите контроль над собственным кодом и процессом его обновления.
  • Используйте lock-файл. Особенно если вы собираетесь использовать continues-integration.
  • Задумайтесь о частном регистре пакетов. Это не только убережет вас от внезапного удаления пакетов, но и ускорит установку зависимостей на билд машине. А это сэкономит ваше время и количество кофе, которое выпивается пока проходит билд во время пулл реквеста.

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


И приношу свои извинения за английские слова, но в русском эквиваленте они сильно режут глаз.

Tags:npmnodejavascriptvulnerability-gтщетность бытия
Hubs: Website development JavaScript Programming Node.JS
+31
10.1k 36
Comments 34
Popular right now