Материал

Как читать lock‑файл перед merge и не пропустить риск

Lock-файл — это не шум на фоне кода. Это карта доверия к вашей зависимости. И именно через неё чаще всего приходит supply chain атака.

Почему lock-файл стал мишенью

Современный фронтенд-проект тянет за собой сотни пакетов. Python-микросервис на Poetry — десятки транзитивных зависимостей. Всё это фиксируется в lock-файле, который по замыслу должен быть машиночитаемым, воспроизводимым и неизменным без явной причины.

Именно поэтому lock-файлы не читают. По данным исследования Snyk 2019 года (которое остаётся базовым для понимания вектора), изменения в lock-файлах на тысячи строк GitHub скрывает за кнопкой «Load diff» — и большинство ревьюеров её не нажимают [1]. Это создало устойчивый слепой угол: пакетная зависимость может быть подменена прямо в PR, и никто не заметит.

В 2025 году ситуация ухудшилась. В сентябре 2025 года были скомпрометированы популярные npm-пакеты debug и chalk вместе с 16 другими библиотеками, суммарно имеющими около 2,6 миллиарда еженедельных загрузок [2]. Вектором стал взлом учётной записи мейнтейнера через фишинг — и вредоносные версии жили в реестре около 2,5 часов, прежде чем были удалены. За это время тысячи команд могли выполнить npm install и получить скомпрометированный пакет прямо в lock-файл.

Позже, в ноябре 2025 года, развернулась кампания Shai-Hulud 2.0, атаковавшая уже сотни npm-пакетов и расширившаяся на PyPI через утечки токенов из GitHub Actions [3].

Lock-файл больше не является «скучным артефактом». Он — поверхность атаки.

Анатомия lock-файла: что именно нужно читать

Изображение

package-lock.json (npm)

Это наиболее информационно насыщенный формат. Для каждого пакета он содержит три критических поля [4]:

version — точная версия, которая будет установлена.

resolved — URL, с которого скачивается tarball. Это поле уникально для npm: в отличие от pnpm, npm хранит конкретный URL источника прямо в lock-файле. Именно это делает npm-экосистему наиболее уязвимой к lockfile injection: злоумышленник может подменить URL на подконтрольный ему репозиторий, сохранив имя и версию неизменными [5].

integrity — SHA-512 хэш содержимого пакета в формате SRI (Subresource Integrity). Если хэш совпадает при установке — пакет не изменён; если нет, установка прерывается с ошибкой [6].

Пример записи из package-lock.json выглядит так:

"node_modules/express": {
  "version": "4.18.2",
  "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
  "integrity": "sha512-5/PsL6iGPdfQ/...",
  "dependencies": { "accepts": "~1.3.8" }
}

На что смотреть при ревью: resolved указывает не на registry.npmjs.org, а на github.com, raw.githubusercontent.com, локальный file:// или вовсе неизвестный хост — это немедленный стоп.

yarn.lock (Yarn 1 и Yarn Classic)

Формат менее читаем и не содержит явного URL в том же виде, что npm, зато в поле resolved также хранится источник скачивания. Ревью yarn.lock затруднено сильнее: GitHub исторически хуже отображает его diff, а сам файл может занимать тысячи строк [1].

Yarn 4 ввёл Hardened Mode — режим, который автоматически активируется для «недоверенных» окружений (например, PR из форков) и запрещает любые изменения lock-файла, не соответствующие метаданным реестра [7].

pnpm-lock.yaml

Интересный случай: pnpm не хранит tarball URL в lock-файле напрямую. Это исключает целый класс lockfile injection атак, связанных с подменой resolved URL. Тем не менее инъекция новых пакетов в pnpm-lock.yaml без их наличия в package.json также была изучена — и pnpm устойчив к этому вектору [8].

poetry.lock (Python / Poetry)

Poetry.lock фиксирует name, version, description, source и, что критично, files — список хэшей для каждого колеса (wheel) и tarball. Проверка хэшей при установке здесь обязательна, что делает poetry.lock одним из более защищённых форматов.

Однако source drift возможен: если в pyproject.toml изменился источник (например, добавлен частный репозиторий), poetry.lock может начать разрешать зависимости с нового источника без явного предупреждения в diff-е. Кроме того, фрагментация Python-экосистемы — pip freeze, Poetry, PDM, pip-tools — исторически порождала проблемы с необязательной проверкой хэшей [9]. PEP 751 в 2025 году предложил стандарт pylock.toml для унификации, пока ещё не ставший мейнстримом [9].

Source drift: когда URL меняется без причины

Source drift — ситуация, когда в lock-файле меняется поле resolved (или source) без изменения версии пакета. Это один из самых незаметных red flag'ов.

Легитимные причины source drift немногочисленны: смена корпоративного npm-прокси, переход с Nexus на Artifactory, добавление private registry. В любом из этих случаев изменение должно быть согласованным, затрагивать сразу все или большинство пакетов и сопровождаться понятным commit message.

Нелегитимный source drift выглядит иначе:

  • Меняется resolved URL одного конкретного пакета без изменения остальных.
  • Новый URL ведёт на GitHub-репозиторий пользователя, а не в реестр.
  • Новый URL использует HTTP вместо HTTPS.
  • Новый URL указывает на неизвестный домен, которого нет в вашем списке доверенных источников.

Именно такую атаку продемонстрировал исследователь Liran Tal в 2019 году: он заменил resolved URL пакета ms в yarn.lock на свой GitHub-репозиторий, сохранив имя и версию нетронутыми. Пакет при этом проходил все поверхностные проверки — имя корректное, версия корректная, тайпсквоттинга нет [1].

Integrity hash: что именно он защищает и где граница

Integrity hash — SHA-512 хэш содержимого пакета. Если при установке скачанный архив не совпадает с хэшем в lock-файле, пакетный менеджер прерывает установку [6].

Это звучит как надёжная защита. Но есть важный нюанс: хэш в lock-файле может быть подменён вместе с resolved URL. Злоумышленник, меняющий resolved URL на подконтрольный сервер, вычислит SHA-512 своего вредоносного пакета и поставит корректный хэш для него [1]. Проверка хэша при установке пройдёт успешно — ведь хэш соответствует скачанному файлу, просто этот файл не является легитимным пакетом.

Таким образом, integrity hash защищает от случайного повреждения пакета в транзите (man-in-the-middle), но не от умышленной подмены записи в самом lock-файле.

Дополнительный вектор: часть CI/CD пайплайнов не проверяет хэши перед установкой, а некоторые прокси-кэши позволяют подменить бинарный файл пакета при сохранении имени и версии в метаданных [10]. Хэш при этом расходится, но если валидация не настроена явно — несоответствие не поймают.

Что именно нужно сделать: убедиться, что CI использует npm ci, а не npm install. Команда npm ci читает lock-файл как единственный источник истины, полностью удаляет node_modules перед установкой и упадёт, если lock-файл не совпадает с package.json [11].

Аналоги для других инструментов:

  • yarn install --frozen-lockfile (Yarn 1) / yarn install --immutable (Yarn 2+)
  • pnpm install --frozen-lockfile
  • poetry install --no-update

Необъяснимые обновления: когда «просто Dependabot» — не ответ

Транзитивная зависимость может обновиться по нескольким причинам: мейнтейнер прямой зависимости выпустил новую версию, которая потянула за собой обновление транзитивной; Dependabot или Renovate автоматически обновил что-то; разработчик вручную запустил npm update.

Необъяснимое обновление — это когда в PR, который, казалось бы, меняет пару строк кода, lock-файл неожиданно показывает изменение 10–50 транзитивных зависимостей. Или одна из зависимостей обновилась с patch-версии до minor без видимой причины.

Red flags необъяснимого обновления:

  • Пакет обновился до версии, опубликованной недавно (менее 3 дней назад). Большинство supply chain атак обнаруживаются и удаляются npm security team в течение 24–48 часов [3]. Если вы устанавливаете пакет возрастом менее 72 часов — вы работаете в окне риска. Именно поэтому pnpm и Yarn 2+ поддерживают настройку minimumReleaseAge.
  • Маленький пакет вдруг добавил postinstall-скрипт, которого раньше не было [12]. Postinstall — один из главных векторов выполнения вредоносного кода при установке.
  • Пакет сменил мейнтейнера незадолго до обновления — это признак атаки через takeover заброшенного пакета [10].
  • Версия в lock-файле отличается от того, что декларировано в package.json, и разница не объяснима правилами semver.

Хорошая практика: не торопиться с обновлениями. Новая версия не означает автоматически безопасная версия [12].

Lockfile injection: механика атаки изнутри

Lockfile injection — это техника, при которой злоумышленник с возможностью сделать PR в репозиторий правит lock-файл так, чтобы пакет устанавливался из подконтрольного ему источника [1].

Сценарий выглядит так:

  1. Злоумышленник форкает репозиторий или получает права контрибьютора.
  2. В PR он меняет resolved URL пакета (например, ms) с https://registry.npmjs.org/ms/... на https://github.com/attacker/ms и подставляет корректный SHA-512 хэш своего вредоносного пакета.
  3. Само изменение package.json при этом выглядит безобидно — или вовсе отсутствует.
  4. Ревьюеры видят diff lock-файла на 500+ строк, скрытый за кнопкой «Load diff», и мержат PR.
  5. Следующий разработчик, запустивший yarn install или npm install, получает вредоносный код в node_modules [1].

Важная деталь о масштабе: GitHub по умолчанию скрывает diff автогенерируемых файлов (именно к ним относятся lock-файлы). Это не баг — это UX-решение, которое создаёт структурный слепой угол в code review [5].

Уязвимость к lockfile injection по экосистемам различается:

  • npm: наиболее уязвим — хранит resolved URL прямо в lock-файле.
  • Yarn 1 (Classic): уязвим аналогично.
  • Yarn 4: защищён Hardened Mode для недоверенных контекстов [7].
  • pnpm: не хранит tarball URL, устойчив к этому вектору [8].
  • Poetry: зависит от конфигурации sources в pyproject.toml; lock-файл фиксирует хэши.

Dependency confusion и source drift: смежная угроза

Отдельный, но связанный вектор — dependency confusion (атака через путаницу пространств имён). Злоумышленник публикует в публичном реестре пакет с тем же именем, что у вашего внутреннего пакета, но с более высоким номером версии. Пакетный менеджер, настроенный работать одновременно с публичным и приватным реестрами, выбирает публичную версию как «более новую» [13].

По данным Orca Security, около 49% организаций имеют хотя бы один актив, потенциально уязвимый к dependency confusion [14].

В lock-файле это проявляется как source drift: пакет, который раньше резолвился с вашего Nexus или Artifactory, внезапно начинает резолвиться с registry.npmjs.org. Именно поэтому проверка resolved URL — не просто теоретическое упражнение.

Практический чек-лист: как читать lock-файл перед merge

Ниже — конкретный операционный список проверок, который применим к любому PR, затрагивающему lock-файл.

Шаг 1: Разворачивайте diff lock-файла, не доверяйте «Load diff»

В GitHub принудительно раскройте diff: нажмите «Load diff» или воспользуйтесь флагом --ignore-generated в локальном git-сравнении. Никогда не мержьте PR с изменёнными lock-файлами, не посмотрев на что именно изменилось.

Шаг 2: Проверяйте resolved URL

Для npm и yarn — пробегитесь по полям resolved в изменённых записях. Допустимые хосты — официальные реестры: registry.npmjs.org, registry.yarnpkg.com, или ваш корпоративный прокси. Всё остальное — вопрос.

Автоматизируйте это через lockfile-lint [8]:

npx lockfile-lint \
  --path package-lock.json \
  --type npm \
  --validate-https \
  --allowed-hosts npm

Инструмент проверит, что все resolved URL используют HTTPS и ведут только на разрешённые хосты. Если найдены исключения — вывод содержит конкретные строки нарушений.

Шаг 3: Ищите неожиданные транзитивные зависимости

Сравните diff на предмет пакетов, которых не было в lock-файле раньше. Если PR меняет одну функцию, а lock-файл приобрёл 20 новых строк с новыми пакетами — это требует объяснения. Запросите у автора PR явное описание: «Почему добавился X? Откуда Y?»

Шаг 4: Проверяйте integrity отдельно при подозрении

Для критичных пакетов можно верифицировать hash вручную:

# Генерация хэша локального tarball
openssl dgst -sha512 -binary < package.tgz | openssl base64 -A

# Сравнение с lock-файлом
cat package-lock.json | jq '.packages["node_modules/critical-package"].integrity'

Шаг 5: Убедитесь, что CI использует строгие команды установки

В CI всегда должны использоваться npm ci, yarn install --frozen-lockfile, или pnpm install --frozen-lockfile. Эти команды отказывают, если lock-файл не соответствует манифесту — это дополнительный gate после merge [11].

Шаг 6: Смотрите на дату публикации новых версий

Если пакет обновился до версии, опубликованной менее 72 часов назад, — уточните причину. Рассмотрите настройку minimumReleaseAge в pnpm или Yarn для автоматического cooldown-периода [3].

Шаг 7: Проверяйте postinstall-скрипты у новых или обновлённых пакетов

cat node_modules/<package>/package.json | jq '.scripts'

Наличие postinstall, preinstall, или install скриптов в пакете, которого раньше не было или у которого этих скриптов не было — сигнал для детальной проверки.

Ошибки и риски, которые чаще всего пропускают

«Это же Dependabot — можно мержить автоматически»

Dependabot и Renovate — полезные инструменты, но они не исключают ни lockfile injection, ни случаи, когда обновлённый пакет оказывается скомпрометирован в реестре. Автоматический merge Dependabot-PR без дополнительных проверок в CI — это принятие риска без его оценки.

«npm audit пройден — значит безопасно»

npm audit проверяет известные CVE из базы данных уязвимостей. Он не проверяет resolved URL, не обнаруживает lockfile injection и не знает о пакетах, скомпрометированных через account takeover, если advisory ещё не появился [8]. Lockfile injection и npm audit решают разные задачи.

«Мы используем приватный registry — мы защищены»

Частично верно: приватный registry снижает риск dependency confusion, если настроен правильно. Но он не защищает от lockfile injection в PR, от компрометации upstream-пакета в публичном реестре (если ваш registry проксирует публичный), и от неправильно настроенного порядка разрешения (public before private) [13].

«Lock-файл — машиночитаемый, ревьюить нечего»

Именно на этом убеждении строится lockfile injection. Злоумышленник рассчитывает, что ревьюер не откроет diff. По данным исследований, даже опытные разработчики упускают подобные изменения [5].

Инструменты для систематической проверки

Ниже перечислены инструменты, которые дополняют ручной ревью (не заменяют его):

lockfile-lint — CLI для npm и yarn, проверяет resolved URL по белому списку хостов и схем. Легко интегрируется в CI и pre-commit hooks [8].

npm ci — строгая установка из lock-файла в CI. Упадёт при любом расхождении lock-файла с package.json. Не использует кэш сети, что снижает риск cache poisoning [11].

pip-audit (Python) — аудит зависимостей через OSV-базу, включая транзитивные. Полезен в паре с poetry.lock.

Yarn 4 Hardened Mode — автоматически активируется для внешних PR на публичных репозиториях. Запрещает lock-файлу содержать метаданные, не соответствующие реестру [7].

Sigstore / Cosign — следующий уровень верификации: криптографическая подпись пакетов позволяет проверить не только хэш, но и провенанс (кто и откуда опубликовал) [5]. Пока не стал стандартом, но внедряется в крупных экосистемах.

vet (SafeDep) — инструмент для детектирования lockfile poisoning в npm, верифицирует соответствие resolved URL структуре пути node_modules и корректность источника [5].

Кейс: как выглядит lockfile injection в реальном PR

Рассмотрим, как такая атака выглядит на практике. Злоумышленник делает PR, который добавляет небольшую фичу в JavaScript-проект. В package.json изменение минимально. Но в yarn.lock среди сотен строк diff — одна запись изменилась: для пакета ms@2.1.3 поле resolved теперь ведёт не на https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz, а на https://github.com/attacker-handle/ms#a7f3c....

Имя пакета — легитимное. Версия — легитимная. Тайпсквоттинга нет. PR выглядит безобидно. Ревьюер видит «diff в lock-файле», нажимает Load diff, видит тысячи строк, закрывает, пишет «LGTM» [1].

После merge следующий yarn install скачивает пакет с GitHub-репозитория злоумышленника. Этот пакет функционально совпадает с оригинальным ms, но содержит дополнительный код в postinstall-скрипте, который отправляет переменные окружения на внешний endpoint.

Именно такой сценарий был публично продемонстрирован как доказательство уязвимости — и атака работала против реальных открытых репозиториев [1].

Будущее: провенанс и подписи как следующий стандарт

Сообщество движется к более надёжным механизмам верификации поверх хэшей:

Sigstore и Cosign переносят парадигму с «хэш совпал» на «я знаю, кто и откуда подписал этот пакет». В Python-экосистеме PyPI начал поддерживать attestation через Sigstore с 2024 года [15].

PEP 751 (Python) вводит стандартизированный pylock.toml с явными источниками, хэшами и временными метками публикации [9]. Это снижает фрагментацию и упрощает аудит.

SBOM (Software Bill of Materials) — машиночитаемый инвентарь всех компонентов и их провенанса — становится требованием в контрактах ряда крупных компаний и госзакупках [16]. Lock-файл — ближайший аналог SBOM для runtime-зависимостей, но без стандартизированного формата и без информации о провенансе.

Эти изменения медленные. До их повсеместного внедрения основной инструмент защиты — дисциплинированный ревью lock-файлов до merge.

Lock-файл как часть релизного gate

Проверка lock-файла — не разовое упражнение. Это часть системного подхода к контролю изменений перед merge и релизом.

Когда PR проходит ревью, а lock-файл при этом остаётся нечитанным — команда принимает зависимости на доверии, а не на проверке. В условиях, когда AI-ассистенты активно ускоряют написание кода и количество PR в день вырастает, этот пробел становится шире: кода больше, ревью быстрее, lock-файлы всё реже читают до конца.

HighVibe — слой контроля до merge, который анализирует именно изменения в конкретном PR: новые и обновлённые зависимости, изменения в конфигурационных и служебных файлах, контекст релиза. Он не заменяет SCA или lockfile-lint, но даёт команде короткий понятный сигнал — что именно в этом PR требует внимания — вместо ещё одного длинного отчёта, который некогда дочитать до конца. Подробнее: highvibe.dev.

Заключение: что делать прямо сейчас

Чтение lock-файла перед merge — навык, который требует системности, а не разового усилия. Практические шаги для команды:

  1. Принять правило: PR с изменениями lock-файла требует явного ревью lock-diff, а не только кода.
  2. Добавить lockfile-lint в CI: проверка allowed-hosts занимает секунды.
  3. Заменить npm install на npm ci во всех CI-пайплайнах.
  4. Настроить cooldown для новых версий через minimumReleaseAge (pnpm, Yarn 4) или политику «не ставить пакет моложе 72 часов без явного обоснования».
  5. При подозрении на source drift — сравнить resolved URL с предыдущим состоянием lock-файла через git diff.
  6. Аудировать Python-зависимости через pip-audit в CI на каждом коммите.
  7. Задокументировать список доверенных реестров и сделать его частью security policy.

Lock-файл — это карта доверия к вашей зависимости. Читайте её до того, как нажмёте «Merge».

Источники

[1] Snyk — "Why npm lockfiles can be a security blindspot for injecting malicious modules" — https://snyk.io/blog/why-npm-lockfiles-can-be-a-security-blindspot-for-injecting-malicious-modules/

[2] SISA Sappers Advisory — "npm Supply Chain Attack Hits Packages With Billions of Weekly Downloads" — https://www.sisainfosec.com/blogs/npm-supply-chain-attack-hits-packages-with-billions-of-weekly-downloads-advisory-by-sisa-sappers/

[3] Emily Xiong / Medium — "Brief History of NPM Supply Chain Attacks in Year 2025" — https://emilyxiong.medium.com/brief-history-of-npm-supply-chain-attacks-in-year-2025-a887dd2e11a4

[4] Sam Atmaramani / Medium — "Solving 'It works on my system' — package.lock.json" — https://medium.com/@s.atmaramani/solving-it-works-on-my-system-package-lock-json-4ce876a763b3

[5] SafeDep / Substack — "Lockfile Poisoning: An Attack Vector to Introduce Malware in Software Supply Chain" — https://safedep.substack.com/p/lockfile-poisoning-an-attack-vector

[6] Node.js Cybersecurity / Medium — "Lockfile poisoning and how hashes verify integrity in Node.js lockfiles" — https://medium.com/node-js-cybersecurity/lockfile-poisoning-and-how-hashes-verify-integrity-in-node-js-lockfiles-0f105a6a18cd

[7] yarnpkg/berry GitHub Discussions — "[Security] Lockfile dependency injection" — https://github.com/yarnpkg/berry/discussions/4136

[8] Liran Tal / GitHub — "lockfile-lint: Lint an npm or yarn lockfile to analyze and detect security issues" — https://github.com/lirantal/lockfile-lint

[9] Cyberpress — "New Python Lock File Standard Aims to Strengthen Dependency Security" — https://cyberpress.org/python-lock-security/

[10] Agentik OS — "Supply Chain Security: SBOMs and Lockfile Attacks" — https://www.agentik-os.com/blog/supply-chain-security-sbom-lockfile-attacks

[11] Upwind — "npm Supply Chain Attack: Massive Compromise of debug, chalk, and 16 Other Packages" — https://www.upwind.io/feed/npm-supply-chain-attack-massive-compromise-of-debug-chalk-and-16-other-packages

[12] Endor Labs — "How to Defend Against NPM Software Supply Chain Attacks" — https://www.endorlabs.com/learn/how-to-defend-against-npm-software-supply-chain-attacks

[13] GitGuardian — "Dependency Confusion Attacks and Prevention: Register Your Private Package Names" — https://blog.gitguardian.com/dependency-confusion-attacks/

[14] Orca Security — "Dependency Confusion: 49% of Organizations Are Vulnerable" — https://orca.security/resources/blog/dependency-confusion-supply-chain-attacks/

[15] Bernát Gábor — "Defense in Depth: A Practical Guide to Python Supply Chain Security" — https://bernat.tech/posts/securing-python-supply-chain/

[16] ReversingLabs — "Software Supply Chain Security Report: A 2025 retrospective" — https://www.reversinglabs.com/blog/sscs-report-2025-retrospective

[17] Undercode Testing — "PackageGate Exposed: How 6 Zero-Days In Your JS Package Manager Threaten The Entire Software Supply Chain" — https://undercodetesting.com/packagegate-exposed-how-6-zero-days-in-your-js-package-manager-threaten-the-entire-software-supply-chain/

[18] Seth Larson — "Defending against the PyTorch supply chain attack PoC" — https://sethmlarson.dev/security-developer-in-residence-weekly-report-25

FAQ

Быстрые вопросы по теме

Что такое lock-файл?

Lock-файл фиксирует версии зависимостей проекта.

Почему lock-файлы важны?

Они помогают предотвратить атаки на зависимости.

Как проверить lock-файл на уязвимости?

Сравните поля resolved и integrity с доверенными источниками.

Что такое source drift?

Это изменение URL источника без изменения версии пакета.

Как защититься от lockfile injection?

Используйте проверенные инструменты и следите за изменениями в lock-файле.

Нужен такой же контроль в ваших изменениях и релизах?

Покажем пример такого замечания на вашем сценарии и соберём пилотный контур без лишней суеты.

Запросить пилот Все статьи
Дальше по теме

Ещё материалы

Все статьи
04 мая 2026 11 мин безопасность

Dependency Confusion: как коллизия имён в реестрах превращается в RCE и что с этим делать

Подзаголовок: Механика private/public name collision, реальные взломы и практический список защит — от naming конвенций до review политики для каждого PR TL;DR Dependency confusion — атака на supply chain: злоумышленник…

Открыть материал
04 мая 2026 15 мин OIDC

OIDC вместо long-lived secrets в CI/CD: как убрать лишние ключи и выстроить least-privilege доступ

Почему static credentials — это системная проблема, и как workload identity federation меняет подход к безопасности пайплайнов TL;DR Long lived AWS access keys, service account JSON и PAT токены в CI/CD — один из основн…

Открыть материал
28 апреля 2026 18 мин AI

Как выбрать инструмент контроля AI-кодинга: 12 вопросов к вендору перед пилотом

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

Открыть материал