Skip to main content

npmの依存関係混同攻撃を検出・防止してサプライチェーンのセキュリティを維持する

著者

2021年9月13日

0 分で読めます

2021年2月9日、Alex Birsan氏は、 aptly named security researchとして知られる依存関係混同に関する調査結果を公開しました。公開内容では、開発者の設定ミスと、オープンソースの言語ベースのソフトウェアエコシステムで使われる複数のパッケージマネージャーの設計上の欠陥を悪用する、新たなサプライチェーン攻撃について説明しています。この攻撃により、Yelp、Tesla、Apple、Microsoftなどの企業にアクセスし、データを持ち出すことができました。

このセキュリティ調査は注目を集め、組織が依存関係混同攻撃の影響を受ける可能性があるかどうかを検出するための新たな幅広いツールが登場しました。

この記事では、npmエコシステムで開発するJavaScriptおよびNode.js開発者に依存関係混同攻撃がどのように影響するのか、手順を追って解説します。また、Snykの新しいオープンソースツールを紹介します。このツールを使えば、自身のソースコードリポジトリに潜む、依存関係混同につながる危険性を検出できます。ツール名はsnyncです。

クイズです!snyncが何を意味するか、わかりますか?答えは記事の最後にあります。

この記事では、このサプライチェーン攻撃が発生するそれぞれのパターンを確認し、各パターンの軽減方法を詳しく説明します。

  • プライベートnpmレジストリの設定ミス

  • プライベートnpmレジストリが最新バージョンを取得する

  • 手動でのパッケージ更新によって悪意のあるバージョンが混入する可能性

依存関係混同攻撃の影響を受ける可能性はありますか?

依存関係混同攻撃が成立するのは、社内のソースコードライブラリに依存している組織に限られます。知的財産を守るためにプライベートパッケージを管理することは一般的です。そのため、多くの組織が社内プロキシ、キャッシュ、プライベートパッケージホスティングのレジストリサービスを利用しています。

組織が社内のプライベートパッケージを管理している場合、そのパッケージは(定義上)パブリックレジストリやそのミラーには存在しません。また、プライベートパッケージがパブリックレジストリに登録されていないため、誰でもそのパッケージ名を取得し、依存関係混同攻撃を仕掛ける可能性があります。プライベートパッケージとパブリックパッケージが同じ名前を持てることが、この攻撃の核心です。

JavaScriptおよびNode.jsのエコシステムでは、予約済みの名前空間としてスコープ付きパッケージを使うことで、依存関係混同攻撃の対象となる範囲を大幅に縮小できます。ただし、npmでもyarnでも、このサプライチェーン攻撃の影響を受ける可能性がある点にご注意ください。

依存関係混同攻撃を再現する

依存関係混同によるサプライチェーン攻撃を実際に試すため、脆弱性とその軽減方法を実演するハンズオンチュートリアルを行います。

以下は、社内で「デス・スター」と呼ばれるプロジェクトのパッケージマニフェストです。いい名前ですよね!

{
  "name": "the-death-star",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node server.js",
  },
  "license": "ISC",
  "dependencies": {
    "debug": "^4.3.2",
    "death-star-secret-hyper-matter-reactor": "^1.0.0",
    “superlaser”: “^1.0.0”
  }
}

このコードはpackage.jsonにあり、このプロジェクトの依存関係も示しています。ご覧のとおり、superlaserは依存パッケージです。もちろん、これは帝国が保有する極秘兵器なので、社内でのみ公開されています。デス・スターは宇宙空間を移動する必要があるため、プライベートパッケージのdeath-star-secret-hyper-matter-reactorにも依存しています。

プライベートnpmレジストリの設定方法

一緒に試す場合は、プライベートnpmレジストリを設定するため、まず本題から少し寄り道します。プライベートレジストリがあれば、自分の環境で実験し、依存関係混同によるサプライチェーン攻撃を最初から最後まで再現できます。

そのために、こうした用途向けのオープンソースプロジェクトVerdaccioを使って、社内プライベートnpmレジストリとプロキシサーバーを設定します。Dockerがインストールされていれば、次のように簡単に起動できます。

docker run -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio\

問題なく起動すると、Verdaccioから次のように表示されます。

ポート4873でVerdaccioを起動するDockerコマンド、読み込まれたプラグイン、HTTPアドレスを示すターミナル出力

これで、ポート4873でローカルに動作する、専用のプライベートnpmパッケージホスティング環境ができました。

それでは、秘密の社内npmパッケージsuperlaserとdeath-star-secret-hyper-matter-reactorを公開しましょう。まず、新しいVerdaccioプライベートレジストリにユーザーを追加します。

npm adduser --registry https://localhost:4873

次に、superlaserパッケージのマニフェストファイルpackage.jsonを用意します。

{
  "name": "superlaser",
  "version": "1.0.0",
  "description": "Our secrets weapon",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
  },
  "license": "ISC"
}

death-star-secret-hyper-matter-reactorのパッケージマニフェストも、パッケージ名が異なるだけで同様です。それでは、プライベートnpmレジストリに公開しましょう。

npm publish  --registry https://localhost:4873

依存関係混同が起きるのはなぜ?

この種のソフトウェアサプライチェーン攻撃につながる主な要因は、次の3つです。

  • 開発者環境またはテストサーバーの設定ミス

  • パブリックnpmレジストリに公開された、より新しいパッケージバージョン

  • パッケージマネージャーの設計上の欠陥とも言える問題

それぞれのケースを改めて見ていきましょう。

プライベートnpmレジストリの設定ミス

開発者または継続的インテグレーション(CI)システムが、社内でsuperlaser依存関係を持つthe-death-star projectのソースコードをクローンすると、この依存関係はどのように取得されるのでしょうか?

npm installコマンドを実行する際、少なくとも次の条件を満たす必要があります。

  1. この社内パッケージが存在するプライベートnpmレジストリのURLが必要です。

  2. そのプライベートレジストリにアクセスするためのトークンなどの認証情報が必要です。

最初の手順で問題が起きる可能性があります。特定のプライベートnpmレジストリを指定するには、npmパッケージマネージャーの設定情報を明示的に指定する必要があります。

いくつかのケースを改めて考えてみましょう。

  1. 継続的インテグレーションシステムにプライベートレジストリが設定されていないと、どうなるでしょうか?

  2. 既存プロジェクトに新たに参加した開発者が、npm config set registryコマンドを実行するなどの事前手順を踏んでいない場合、どうなるでしょうか?

  3. 誤って.npmrc設定を削除または変更し、社内プライベートnpmレジストリを指定しなくなった場合、どうなるでしょうか?

このように、社内レジストリのカスタム設定が省略されると、npmパッケージマネージャーはパブリックレジストリ(registry.npmjs.org)を既定で使用し、そこからパッケージをダウンロードします。

パブリックnpmレジストリには誰でもパッケージを公開できます。そのため、悪意のあるユーザーがsuperlaserという名前のパッケージを公開すると、社内パッケージではなく、そのパッケージがダウンロードされてインストールされてしまいます。

npmの依存関係混同を防ぐ方法

根本的な問題は、プライベートnpmプロキシが適切に設定されていないことです。開発者やCIシステムにこの設定がなければ、攻撃を受ける可能性があります。

まず、必ず.npmrcファイルを利用可能にするか、別の方法でプライベートnpmプロキシを設定してください。

次に、名前空間がパブリックnpmjsレジストリで予約されていないプライベートパッケージを使用していないか、事前に検出する方法があります。そのためにsnyncを開発しました。依存関係をインストールする前に、CIサーバーの実行手順の一部として実行できます。これにより、誤って悪意のあるパッケージをインストールするのを防げます。

次のスクリーンショットでは、snyncをnpx経由で実行し、依存関係をスキャンする対象として現在のディレクトリを指定しています。また、superlaserという名前のパッケージがプライベートパッケージであることも指定しています。

snycが依存関係をスキャンし、1つが脆弱、もう1つが不審と示されているターミナル出力。

結果を見ると、snyncは依存関係混同につながる可能性のある、次の2つの問題を検出しました。

  1. death-star-secret-hyper-matter-reactorパッケージはvulnerableです。現時点で、パブリックnpmjsレジストリに同名のパッケージが登録されていないためです。つまり、誰でもその名前で登録でき、依存関係混同攻撃が発生する可能性があります。

  2. superlaserパッケージはsuspicious状態です。つまり、このツールは次のいずれかのケースを検出しました。

    1. このパッケージ名はGitのソースコードに先に登場し、その後、同名のパッケージがパブリックnpmjsレジストリに公開されました。パブリックnpmjsレジストリのパッケージが悪意のあるものだという意味ではありませんが、確認が必要です。

    2. 同名のプライベートパッケージを作成するより前から、そのパッケージ名がパブリックnpmjsレジストリに存在しています。

snyncはNode.jsベースのオープンソースコマンドラインプロジェクトです。DevSecOpsのセキュリティパイプラインでぜひご活用ください。

プライベートnpmレジストリが最新バージョンを取得する

同じ名前のパッケージ(superlaser)がパブリックnpmレジストリで公開されていて、より高いsemverバージョンが利用可能な場合、どうなるでしょうか?

状況を例にすると、次のとおりです。

ここで、新しいプロジェクトを作成し、superlaserパッケージのインストールを要求するとどうなるでしょうか?まだpackage.jsonもロックファイル(package-lock.json)もありません。開発者が単に次のように実行したとします。

npm install superlaser

このインストールでは、リモートの攻撃者が管理する悪意のあるバージョンのsuperlaserがインストールされる可能性があります。しかし、なぜでしょうか?開発者はローカルのnpmレジストリを設定しているはずです。

テストで確認されたように、社内のプライベートnpmプロキシが設定されていても、多くのプロキシはまずパブリックnpmレジストリで最新バージョンを確認することがわかっています。より新しいバージョンがある場合、これらのプロキシはパブリックレジストリから最新のsemverバージョンを取得してインストールします。

Verdaccioでこの状況を再現してみましょう。以下のように、害のないsuperlaser npmパッケージをVerdaccioにプッシュしました。Verdaccioは、社内プライベートnpmパッケージのホスティングとして機能します。

パッケージ「superlaser」のバージョン1.0.0、公開情報、ログイン操作が表示された、暗い配色のVerdadcciaブラウザページ

次に、ローカルのVerdaccioレジストリを指定した.npmrcファイルしかない新しいプロジェクトディレクトリで、npm installコマンドを実行するとどうなるか見てみましょう。社内で公開したsuperlaser@1.0.0がインストールされると思っていたにもかかわらず、superlaserパッケージの最新バージョンがパブリックnpmレジストリから取得されます。

superlaserのnpm install、postinstallメッセージ、およびthe-death-starパッケージの警告を表示するターミナル。

予期しない結果となり、エンドユーザーを危険にさらす可能性があります。

この動作については、オープンソースのVerdaccioプロジェクトでGitHub上の公開ディスカッションが行われています。参加してこのトピックの議論を追いたい方はご覧ください。

技術的には、Verdaccioなどのプライベートnpmプロキシが行うこの処理では、次のような複数の変数が考慮されます。

  1. 最新のsemverリリース

  2. パッケージの公開日時

たとえば、パブリックnpmjsレジストリに高いsemverバージョンが存在していても、同じ名前で、その公開バージョンと同じ範囲内の低いsemverバージョンのパッケージが、高いバージョンの公開日より後に作成された場合、Verdaccioはパブリックレジストリのパッケージを取得しません。

誤ったパッケージの取得を防ぐ方法

プライベートnpmプロキシを設定し、パブリックレジストリへの上流プロキシリクエストを常に無効にするようにします。パッケージまたはバージョンがローカルで利用できない場合は、信頼性や検証が不十分なソースから無条件にパッケージを取得しない方法で解決してください。

ここでの例のようにVerdaccioを使用している場合は、/verdaccio/conf/config.yamlにある次の設定で対応できます。

#
# This is the config file used for the docker images.
# It allows all users to do anything, so don't use it on production systems.
#
# Do not configure host and port under `listen` in this file
# as it will be ignored when using docker.
# see https://verdaccio.org/docs/en/docker#docker-and-custom-port-configuration
#
# Look here for more config file examples:
# https://github.com/verdaccio/verdaccio/tree/master/conf
#

# path to a directory with all packages
storage: /verdaccio/storage/data
# path to a directory with plugins to include
plugins: /verdaccio/plugins

web:
  # WebUI is enabled as default, if you want disable it, just uncomment this line
  #enable: false
  title: Verdaccio
  # comment out to disable gravatar support
  # gravatar: false
  # by default packages are ordercer ascendant (asc|desc)
  # sort_packages: asc
  # darkMode: true

# translate your registry, api i18n not available yet
# i18n:
# list of the available translations https://github.com/verdaccio/ui/tree/master/i18n/translations
#   web: en-US

auth:
  htpasswd:
    file: /verdaccio/storage/htpasswd
    # Maximum amount of users allowed to register, defaults to "+infinity".
    # You can set this to -1 to disable registration.
    # max_users: 1000

# a list of other known repositories we can talk to
uplinks:
  npmjs:
    url: https://registry.npmjs.org/

packages:
  '@*/*':
    # scoped packages
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    # DO NOT FETCH PACKAGES FROM NPMJS
    #proxy: npmjs

  '**':
    # allow all users (including non-authenticated users) to read and
    # publish all packages
    #
    # you can specify usernames/groupnames (depending on your auth plugin)
    # and three keywords: "$all", "$anonymous", "$authenticated"
    access: $all

    # allow all known users to publish/publish packages
    # (anyone can register by default, remember?)
    publish: $authenticated
    unpublish: $authenticated

    # if package is not available locally, proxy requests to 'npmjs' registry
    # DO NOT FETCH PACKAGES FROM NPMJS
    #proxy: npmjs

middlewares:
  audit:
    enabled: true

# log settings
logs:
  - { type: stdout, format: pretty, level: http }
  #- {type: file, path: verdaccio.log, level: info}
#experiments:
#  # support for npm token command
#  token: false
#  # support for the new v1 search endpoint, functional by incomplete read more on ticket 1732
#  search: false

# This affect the web and api (not developed yet)
#i18n:
#web: en-US

上記はDockerコンテナ版Verdaccioの標準設定ファイルです。ただし、# DO NOT FETCH PACKAGES FROM NPMJSというコメントがあり、その次の行でproxy: npmjsオプションがコメントアウトされている点が異なります。これにより、一致するパターンに設定されたパッケージについて、Verdaccioがnpmjsから何も取得しないようにできます。

手動でのパッケージ更新により、悪意のあるバージョンが導入される可能性があります

npm update またはnpm install <packages>@latestを実行してnpmパッケージを手動で更新し、依存関係を最新のバージョンにするケースを考えてみましょう。

こうした更新手順を実行すると、先ほどと同じことが起こります。npm updateコマンドはプライベートnpmプロキシに最新バージョンの取得を要求し、プロキシは公開npmレジストリで最新バージョンを確認します。

yarnを使用している場合、yarn upgradeを実行すると、公開npmjsレジストリから悪意のある可能性があるパッケージが取り込まれ、同じ結果になります。

次の例で説明します。まず、内部のsuperlaser@1.0.0バージョンから始めます。

{
  "name": "new-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "superlaser": "^1.0.0"
  }
}

ローカルレジストリを定義し、稼働中のVerdaccioサーバーを指定する.npmrcファイルを用意しています。それでも、依存関係をすべて最新にするためにnpm updateを実行すると、公開npmjsレジストリから、semverの条件に一致する最新バージョンが取り込まれることがわかります。

npm updateでsuperlaser@1.99999.999がインストールされ、postinstallメッセージと監査結果が表示されているターミナル出力

どうすれば防げるでしょうか?

npmパッケージを手動で無条件に更新するのではなく、オープンソースプロジェクトのリポジトリにプルリクエストを作成する自動パッケージ更新を利用しましょう。これにより、package-lock.jsonやyarn.lockなどのパッケージマニフェストも同期されます。

Snykを使えば、npmパッケージの更新を自動化できます。以下は、マージされたプルリクエストのスクリーンショットです。

中程度の深刻度の安全でないベクターの問題を修正するために、Snykがnode-uuidを1.4.0から1.4.8にアップグレードするGitHubプルリクエスト。

Snykが作成するプルリクエストの数を制限したり、特定のパッケージの更新を完全に無視したりするなど、自動更新の設定をさらに細かく調整できます。詳しくは、Snykのドキュメント自動PRによる依存関係のアップグレードをご覧ください。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。

アプリケーションセキュリティに関するリソースのまとめ

ここまで読んでくださった方には、記事の冒頭で出題したクイズの答えをお伝えします。オープンソースツールの名前「snync」は、So Now You’re Not Confused(もう混乱していない、という意味)の略です。正解できましたか?

サプライチェーンのセキュリティインシデントが発生している昨今、コードを書くときも、日々の開発者セキュリティの習慣においても、安全な手法を実践することの重要性はいくら強調してもしすぎることはありません。JavaScriptやNode.jsを日常的に使用している方やチームには、次の参考資料がおすすめです。

  1. GitHubセキュリティのベストプラクティス10選

  2. npmセキュリティのベストプラクティス10選

  3. Dockerを使ってNode.jsウェブアプリケーションをコンテナ化するためのベストプラクティス10選

続きを読む

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに対応するセキュリティ

AI駆動型開発のスピードに、従来のセキュリティワークフローは追いついていません。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

blog feature ai violet
Blog

VulnBump:AIエージェントはビルドを壊さずに脆弱な依存関係を修正できるか?

VulnBumpは、実際の依存関係アップグレード307件を対象にAIエージェントをテストします。脆弱性が解消され、ビルドとテストが成功した場合にのみ、修正としてカウントされます。

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。