В июне этого года RubyGems, основной репозиторий пакетов Ruby (gems), объявил, что многофакторная аутентификация (MFA) будет постепенно распространяться среди пользователей. Это означает, что пользователи в конечном итоге должны будут входить в систему с помощью одноразового пароля со своего устройства аутентификации, что значительно сократит количество захватов учетных записей.

Команда, в которой я прохожу практику, команда Ruby Dependency Security в Shopify, сыграла большую роль в распространении MFA среди пользователей RubyGems. Миссия команды – повысить безопасность цепочки поставок программного обеспечения Ruby, поэтому увеличение использования MFA – это то, что мы хотели помочь реализовать.

Одним из интересных решений, с которым столкнулась команда RubyGems, было определение того, кто будет включен в первый этап. Команда хотела включить по крайней мере 100 лучших пакетов RubyGems, но также хотела предотвратить выпадение пакетов (и людей) из этой когорты в будущем.

Чтобы соответствовать этим критериям, команда установила порог в 180 миллионов загрузок для драгоценных камней. Как только драгоценный камень преодолевает 180 миллионов загрузок, его владельцы обязаны использовать многофакторную аутентификацию в будущем.

Это решение привело меня к любопытству. Поскольку пакеты часто зависят от других пакетов, могут ли некоторые из этих больших (более 180 миллионов загрузок) пакетов зависеть от маленьких (менее 180 миллионов загрузок) пакетов? Если бы это было так, то появилась бы небольшая лазейка: если бы хакер хотел максимально расширить свой охват в экосистеме Ruby, он мог бы выбрать один из этих маленьких пакетов (который устанавливался бы каждый раз, когда кто-то устанавливал один из больших пакетов), обходя защиту MFA больших пакетов.

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

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

Расследование

Первым шагом в определении того, зависят ли большие пакеты от маленьких, было получение списка больших пакетов. На странице статистики rubygems.org показаны 100 лучших пакетов по количеству загрузок, но последний пакет на 10-й странице имеет 199 миллионов загрузок, что означает, что соскабливание этих страниц даст неполный список, поскольку порог, который меня интересовал, составляет 180 миллионов загрузок.

Чтобы получить полный список, я воспользовался дампами данных, которые предоставляет сайт rubygems.org. По сути, сайт делает ежедневный снимок базы данных rubygems.org, удаляет любую конфиденциальную информацию, а затем публикует ее. В их репозитории есть удобный скрипт, который позволяет загружать эти дампы данных в вашу собственную локальную базу данных rubygems.org и, соответственно, выполнять запросы к этим данным с помощью консоли Rails. Мне потребовалось много попыток, чтобы сделать запрос, который получил бы все большие пакеты, но в конце концов я нашел тот, который сработал:

Rubygem.joins(:gem_download).where(gem_download: {count: 180_000_000..}).map(&:name)

Теперь у меня был список из 112 больших драгоценных камней, и мне нужно было найти их зависимости. Первым методом, который я попробовал, было использование API rubygems.org. Как описано в документации, вы можете дать API имя драгоценного камня, и он выдаст вам имена всех его зависимостей в качестве части полезной нагрузки ответа. Та же конечная точка этого API также сообщает вам, сколько загрузок имеет гем, поэтому путь был ясен: для каждого большого гема получить список его зависимостей и выяснить, не имеет ли какая-либо из них меньше загрузок, чем порог.

Вот функции, которые получают зависимости и загрузки:

Собрав все это вместе, я обнаружил, что 13 из 112 больших драгоценных камней имеют в качестве зависимостей маленькие драгоценные камни. Исключения! Почему же эти маленькие драгоценные камни имели меньше загрузок, чем их родители? Я узнал, что это произошло в основном по двум причинам:

Теперь у меня было доказательство существования больших камней, которые были бы косвенно уязвимы для поглощения аккаунта маленького камня. Хотя доказательство существования – это хорошо, мне было указано, что API rubygems.org возвращает только список, символизирующий прямые зависимости гема, и что эти зависимости могут иметь подзависимости, которые я не проверял. Как же мне узнать, какие пакеты устанавливаются при установке одного из этих больших драгоценных камней?

Конечно же, с помощью Bundler!

Bundler – это менеджер зависимостей Ruby, с которым, вероятно, знакомо большинство пользователей Ruby. Bundler получает список драгоценных камней для установки (Gemfile), устанавливает зависимости, удовлетворяющие всем требованиям к версии, и, что очень важно для нас, составляет список всех этих зависимостей и версий в файле Gemfile.lock. Итак, чтобы узнать, какие большие драгоценные камни каким-либо образом зависят от маленьких, я программно создал Gemfile, в котором был только большой драгоценный камень, программно запустил bundle lock и программно прочитал созданный Gemfile.lock, чтобы получить все зависимости.

Вот функция, которая выполнила всю работу с Bundler:

С помощью этой новой методологии я обнаружил, что 24 из 112 больших драгоценных камней зависят от маленьких драгоценных камней, что составляет довольно значительную их часть. Обнаружив это, я захотел изучить визуализацию. До этого момента я просто выводил результаты в командную строку, создавая текстовые дампы, подобные этому:

Эта визуализация не очень удобна для чтения, и в ней упускаются закономерности. Например, как вы можете видеть выше, многие большие гемы полагаются на racc. Было бы полезно узнать, зависят ли они от него напрямую, или большинство пакетов зависят от него косвенно, через какой-то другой пакет. Идея создания графа витала в моей голове с самого начала этого проекта, и когда я понял, насколько это может быть полезно, я решился на это. Я использовал graph gem, следуя некоторым примерам из этого выступления Аджи Хаммерли. Я использовал поиск по широте, начав с очереди всех крупных драгоценных камней, добавляя прямые зависимости в очередь по мере продвижения. Я добавил края от драгоценных камней к их зависимостям и выделил маленькие драгоценные камни красным цветом. Вот первая итерация:

Оказалось, что существует множество драгоценных камней AWS, поэтому я решил удалить их из графа и получил гораздо более приятный результат:

График, несмотря на умеренную загроможденность, лаконично отображает много информации. Например, в центре слева вы видите галактику самоцветов, а гравитационным аттрактором являются рельсы – явный краеугольный камень в мире Ruby.

Узел с наибольшим количеством стрелок, указывающих на него, – это activesupport, так что это действительно активная поддержка.

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

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

Вот пример распечатки:

Я добился этого, создав тип данных направленного графа и написав алгоритм поиска в глубину, чтобы найти все пути от одного узла к другому. Я решил создать свой собственный тип данных, потому что поиск всех путей на графе, насколько я могу судить, еще не реализован ни в одном камне Ruby. Вот алгоритм, если вам интересно (@graph – это хэш из пар String:Array, по сути, список смежности):

Что дальше

В общем, я нашел четыре способа ответить на вопрос, зависят ли большие драгоценные камни от маленьких:

Я доволен своей работой, и я рад, что мне удалось узнать о файловом вводе-выводе и использовать теорию графов. Я все еще относительно новичок в Ruby, поэтому подобные проекты очень дидактичны.

Остается вопрос, что делать с 24 технически небезопасными гемами. Одно из предложений – ничего не делать, поскольку в конечном итоге всем придется включить MFA, а захват учетных записей – все еще редкое событие, несмотря на то, что число таких случаев растет. 

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

В любом случае, необходимо дополнительное обсуждение с моей командой. Спасибо за чтение!

Где бы вы ни были, ваше следующее путешествие начинается здесь! Если вас интересует создание систем с нуля для решения реальных проблем, в нашем инженерном блоге есть истории о других проблемах, с которыми мы сталкивались. Заинтригованы? Посетите нашу страницу “Карьера инженера”, чтобы узнать об открытых вакансиях и познакомиться с Digital by Design.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *