Скрывает ли ваш lockfile уязвимости?

Lockfile — самый честный документ в вашем репозитории: в нём перечислены точные версии каждого пакета, который реально устанавливает ваша сборка, без диапазонов, без «latest», без сюрпризов. Но он же, тихо, — список поверхности атаки: каждая закреплённая версия — это версия, в которой кто-то мог найти дыру с тех пор, как вы её закрепили. В этой статье объясняется, как сканер превращает этот список в ответы: как база OSV сопоставляет версии с известными уязвимостями, почему флаг CISA KEV важнее балла CVSS и почему «уязвимостей не найдено» — это утверждение о базе данных, а не о вашем коде. Проверить это можно самостоятельно в SBOM Vulnerability Scanner, который разбирает ваш lockfile в браузере и никогда его не загружает.

Что закрепляет lockfile — и почему это поверхность атаки

Современное программное обеспечение — это по большей части чужой код. Типичное веб-приложение подтягивает сотни пакетов, а каждый из них — ещё больше: прямая зависимость от веб-фреймворка легко превращается в двести транзитивных пакетов, которые вы никогда не выбирали и не проверяли. Lockfile — это запись этого дерева: package-lock.json для npm, полностью закреплённый requirements.txt для Python, go.mod и go.sum для Go, — записанная так, чтобы каждая установка на каждой машине давала одно и то же дерево. Без него «у меня на машине работает» — это надежда, а не факт.

Та же самая точность делает lockfile правильным местом для поиска известных уязвимостей. База уязвимостей индексируется по имени пакета, экосистеме и точной версии: lodash 4.17.20 уязвим, lodash 4.17.21 — нет, и разница определяется одним запросом. Плавающий диапазон так оценить нельзя — сканеру пришлось бы угадывать, какую версию вы реально установили, а угадывание — не практика безопасности. Поэтому первое правило гигиены lockfile простое и не подлежащее обсуждению: коммитьте lockfile, держите его закреплённым и сканируйте закреплённое дерево, а не список желаний в вашем манифесте.

Как OSV сопоставляет версии с известными уязвимостями

OSV.dev — это публичная база уязвимостей, которая агрегирует advisories из GitHub, NVD и языковых экосистем в один индекс с возможностью запросов. Сканер отправляет ей пакет троек «имя, экосистема, версия» — lodash, npm, 4.17.20 — и получает обратно идентификаторы уязвимостей, затрагивающих именно эту версию. Затем идентификаторы загружаются полностью: описание, затронутые диапазоны с их исправленными версиями, серьёзность в виде вектора CVSS и псевдонимы CVE. С запросом не уходит ничего о вашем проекте — только публичные координаты публичных пакетов.

Совпадение намеренно примитивное, и в этом его сила. Оно не читает ваш код, не оценивает, вызываете ли вы уязвимую функцию, и ему всё равно, как используется пакет. Если в advisory сказано, что затронуты версии до 4.17.21, а вы закрепили 4.17.20, вы совпадаете — даже если ваш код никогда не касается уязвимой функции. Такое избыточное срабатывание — это преимущество: анализ достижимости — более сложный и медленный вопрос, а дешёвый вопрос («известна ли эта версия как плохая?») уже ловит зависимость, которую вы забыли обновить три года назад.

Чего сканер не видит

У проверки, основанной на сопоставлении версий, есть жёсткая граница, и её стоит назвать прямо: она может найти только уязвимости, о которых кто-то уже сообщил, для версии, известной базе. Уязвимость, обнаруженная вчера и ещё не опубликованная, невидима. Пакет, который никогда не был уязвим, но вредоносен — тайпосквоттер популярного имени, скомпрометированный релиз, зависимость, которая передаёт данные домой, — вообще не имеет CVE, и никакой сканер такого рода его не пометит. То же касается вашего собственного кода: сканер оценивает пакеты, которые вы устанавливаете, а не код, который вы пишете.

Именно поэтому результат «уязвимостей не найдено» нужно читать внимательно. Он означает: для этих точных версий, в этой базе данных, на данный момент ничего не зарегистрировано. Это полезный отрицательный результат — чистое сканирование свежего lockfile действительно хорошая новость, — но это не сертификат. Защита от того, чего сканер не видит, — это другие инструменты: закрепление и проверка новых зависимостей до того, как они попадут в дерево, непрерывное отслеживание новых advisories, а не раз в квартал, и подозрительное отношение к внезапному новому релизу тихого пакета. Сканер — это дымовой датчик, а не пожарная команда.

Почему флаг KEV важнее высокого балла CVSS

Балл CVSS отвечает на гипотетический вопрос: насколько плоха была бы эта уязвимость, если бы кто-то её эксплуатировал? Каталог известных эксплуатируемых уязвимостей CISA отвечает на факт: эксплуатирует ли её кто-то в реальных условиях прямо сейчас? Эти два ответа регулярно расходятся. Уязвимость с оценкой 9.8, которую никто не превратил в оружие, может подождать до следующего планового обновления; уязвимость с оценкой 5.3, которая есть в списке KEV — со сроком устранения, установленным CISA, — заслуживает исправления на этой неделе, потому что эксплойт уже существует и уже используется.

Именно эта инверсия — причина, по которой сканер отдельно помечает строки KEV, вместо того чтобы сводить их в сортировку по серьёзности. Порядок сортировки говорит вам, что плохо; значок KEV говорит вам, что срочно. На практике сортировка короткая: сначала исправьте элементы KEV, затем критические с опубликованной исправленной версией, а остальное запланируйте на следующий цикл обновлений. Bulk CVE Triage на этом сайте помещает те же CVE в контекст эксплуатации — статус KEV, вероятность EPSS и CVSS вместе, — когда одного значка недостаточно, чтобы обосновать работу команде.

Коммитьте lockfile, сканируйте его в CI — а не раз в квартал

Lockfile, который не закоммичен, — это lockfile, которого не существует. Если он живёт только на машине одного разработчика, каждая другая машина устанавливает то, во что «latest» разрешается в этот день, и дерево, которое вы просканировали, — не то дерево, которое вы поставляете. Его коммит — самый дешёвый контроль цепочки поставок: он делает установки воспроизводимыми, делает ответы сканера верными для всех и превращает обновление зависимости в рецензируемый diff вместо молчаливого дрейфа.

Вторая половина — это ритм. Уязвимости публикуются каждый день, поэтому сканирование раз в квартал — это снимок, который стареет за дни. Полезная форма — сканирование при каждом pull request, затрагивающем lockfile, плюс плановое сканирование основной ветки — тот же цикл, что и у набора тестов, с тем же свойством: новое advisory делает зелёную сборку красной в день публикации, когда исправление — это повышение версии, а не разбор после инцидента. Браузерный сканер на этом сайте — ручная версия этого цикла: вставьте lockfile после обновления от dependabot или перед релизом и подтвердите дерево, которое собираетесь поставлять.

Исправленные версии, ломающие изменения и когда исправлять, а когда закреплять

Каждое совпадение в сканировании несёт исправленную версию — первый релиз, в котором уязвимости нет, — или честное «не опубликовано». Обычный случай — патч- или минор-релиз, и обновление — это изменение в одну строку в lockfile. Но исправленная версия не всегда безопасна для принятия: переход на мажорную версию может изменить поведение, от которого зависит ваш код, а исправление, выпущенное вчера, прошло лишь день реального тестирования. Решение — то же, что и при любом обновлении зависимости: прочитайте changelog, запустите тесты и сначала обновите пакеты, которые вы реально импортируете.

Когда исправленной версии нет, вариантов остаётся три: удалить зависимость, если она никому не нужна, заменить её поддерживаемой альтернативой или смягчить уязвимый путь в коде — перестать передавать недоверенные данные в затронутую функцию или поставить вызов за проверку, описанную в advisory, — и следить за обновлениями advisory. Чего делать не следует, так это закреплять уязвимую версию и забывать о ней: закрепление — это решение, и оно заслуживает комментария в lockfile или в трекере задач с объяснением почему и датой пересмотра. После любого изменения просканируйте заново; сканер занимает секунды, и второе сканирование — единственное доказательство того, что находки первого сканирования исчезли.

Источники

Часто задаваемые вопросы

Сканирование ничего не нашло. Мой проект безопасен?

Это хорошая новость, а не гарантия. Чистое сканирование означает, что для ваших точных закреплённых версий в базе OSV не зарегистрировано уязвимостей. Оно ничего не говорит о незарегистрированных уязвимостях, вредоносных пакетах или вашем собственном коде. Держите lockfile закоммиченным, просканируйте его заново, когда он изменится, и относитесь к сканированию как к одному слою защиты — SBOM Vulnerability Scanner сам объясняет свою границу на странице.

Зачем сканеру точные версии?

Потому что базы уязвимостей индексируются по точной версии. Диапазон вроде >=1.2 или ~=2.0 не определяет одну версию, поэтому сканер не может сказать, затронута ли версия, которую вы реально установили. Закрепляйте точные версии в lockfile — именно для этого lockfile и существуют, — и сканер сможет оценить каждую запись одним запросом.

Исправление потребует перехода на мажорную версию. Стоит ли всё равно исправлять?

Для уязвимости из списка KEV — да: сначала исправьте, а с поломкой разбирайтесь потом, потому что эксплойт уже используется. Во всех остальных случаях взвесьте серьёзность против поломки: обновите пакеты, которые ваш код реально импортирует, запустите тесты, а если мажорный переход слишком рискован, смягчите уязвимый путь в коде и запланируйте обновление. После изменения просканируйте заново, чтобы подтвердить, что находка исчезла.