🛡️

npmパッケージ/GitHub Actionsを利用する側/公開する側でサプライチェーン攻撃を防ぐためにやることメモ

に公開

パッケージを利用する側、パッケージを公開する側でサプライチェーン攻撃を防ぐためにできることのメモ書きです。

パッケージを利用する側

npmやGitHub Actionsなどを利用する側として、サプライチェーン攻撃を防ぐためにできることをまとめます。

ロックファイルを使う

  • npmやYarn、pnpmなどのパッケージマネージャーは、依存関係のバージョンを固定するためにロックファイル(例: package-lock.json, yarn.lock, pnpm-lock.yaml)を使用する

GitHub ActionsではSHA Pinを行う

GitHub Actionsには最小の権限を与える

依存のインストール

postinstallなどのLifecycle scriptsをインストールに実行しないようにする。
多くのnpmで配布されるマルウェアはpostinstallでコードを実行して、機密情報を盗み出すといった攻撃を行う。(npm debug and chalk packages compromisedのようにブラウザで実行するタイプなどもあるが、攻撃がワンテンポ遅れるので成功確率も下がる)

pnpm 10では、Lifecycle Scriptsがデフォルトでは実行されない挙動となっていて、必要なパッケージのみpnpm approve-scriptなどで許可する方式になっている。

bunでは、許可リスト方式で一部は自動的に許可されるが、他のパッケージはLifecycle Scriptsがデフォルトでは実行されないようになっている。

npmなどでは--ignore-scriptsが同様のオプションだが、npmなどは特定のパッケージのみを許可することが難しいので、pnpm以外では実際には運用するのは難しいと思う。

依存のアップデート

サプライチェーン攻撃起きてからすぐパッケージをアップデートしてしまうと受動的に攻撃を受ける可能性がある。
そのため、パッケージが公開されてから1週間経ってからアップデートするPRを作るようなオプションをdependabotやrenovateでは有効にする。

追記: pnpm 10.16でminimumReleaseAgeという同様の役割のオプションが追加された
pnpmの場合は、インストールなどあらゆる状況に影響するオプションとなっているので、セキュリティアップデートなどを考えると1日などある程度の長過ぎないような値が現実的となる。

renovatebot/dependabotを装ってlockfileを意図しない形でアップデートする攻撃も考えられる。この場合 dependabotなら手動でマージボタンは押さないで @dependabot merge コメントをすることで、本物のdependabotかを見分けることができる(権限がない偽物ならマージはできないため)
renovatebotだとこれに対応する方法はないけど、最近はpnpmを使っているのであまり気にしなくなった。この問題をチェックするツールも存在する。

pnpm catalogではlockfileのズレが pnpm install時にエラーにならないバグがあるけど、workaroundで検出はできる

スキャンを通してないパッケージをnpxで叩かない

  • MCP系で特に問題になる
  • npxはバージョン指定がないと、最新のパッケージを自動でダウンロードして実行してしまうので、パッケージが安全なのかを確認してから実行する必要がある
  • 繰り返し実行するなら、そもそもnpxでダウンロードしないで、devDependenciesに入れておいてロックするなどバージョンを固定する
  • npxで叩く場合は、バージョンを指定する e.g. npx example-package@1.2.3
    • パッケージの孫依存(Transitive dependency)は固定されないが、latestよりは良い
  • azu/ni.zshryoppippi/bun-socket-scannerでは、Socket.devを使ったスキャンを行ってからパッケージを実行できる
  • npmnpxなどの既存のコマンドをラップする形でスキャンするツールだとsocket npm & socket npxAikidoSec/safe-chainなどがあります

AI AgentはSandboxで実行する

  • 勝手に外部パッケージを入れたり色々しちゃうため
  • サンドボックス環境で実行することで、ホストOSへの影響を最小限に抑える
    • 主にたどれるべきではないファイルやネットワークにアクセスされるのを防ぐ
  • macOSなら軽量なサンドボックスとしてsandbox-execを使う
  • DevContainersなどDockerコンテナで実行する

ざっくりまとめると多層防御的な話になるので、次のようなイメージです。
難しそうな感じはしますが、普通に使えば普通にやってくれるツールは増えているので、そこまで無理なくやっていけるかなという感じはしています

  1. ロック(version pinning / lockfile 固定)
  2. 事前スキャン(sockets.dev 等で既知リスク検出)
  3. インストール (Lifecycle Scriptsを実行しない、minimumReleaseAge)
  4. アップデート(Renovate / Dependabot の cooldown: 7日)
  5. 検証・改ざん検出(署名 / checksum / lockfile 整合性/ SBOM / 差分検証)

パッケージ利用のレイヤー


パッケージを公開する側

npm registryにパッケージを公開する側として、サプライチェーン攻撃を防ぐためにできることをまとめます。
攻撃者に認証情報を盗まれないようにする方法、アカウントを乗っ取られた場合の被害を軽減する方法などについて

npmの今後の方針については次の記事にまとまっています。

コミットはsecretlintを通す

  • secretlintを使って、コミットに秘密情報が含まれていないかをチェックする
  • 機密情報はprivate repositoryであっても漏洩する可能性があるので、コミットに含めない

ローカルに生tokenを置かない

TokenのScopeを最小にする

  • npmはGranular access tokens
  • GitHubはGitHub AppsFine-grained personal access tokens
  • それぞれ最小のスコープを設定したtokenを利用する
  • npmはほぼtokenが不要になってきているので、できる限りtokenを発行しないようにする
  • GitHubは最小のスコープ(リポジトリと権限)のtokenを発行する
  • 広いスコープ(リポジトリ)のtokenはghコマンドぐらいになるイメージで、このtokenも1password連携を使うことでローカルに生のtokenとして存在しない状態にできる

GitHub ActionsはSecurity Checkを行なってからマージする

  • GitHub Actionsには典型的なScript Injectionの問題がある
  • - run: echo "${{ github.event.issue.title }}" のように書いたStepがあると、Issueのタイトルに悪意のあるコードを書かれると、そのコードが実行されてしまう
  • https://securitylab.github.com/resources/github-actions-untrusted-input/
  • そのため、GitHub ActionsのWorkflowを変更するPRは、Security Checkを行なってからマージする
  • 背景としては、AdnaneKhan/gato-xのようなツールで、このような脆弱性があるGitHub Actionsは自動的に見つけることができる
    • 実際にnxの攻撃の起因となったGitHub Actionsの問題も、実際にインシデント起きる1週間前には発見されている
    • これらのツールを回して脆弱なPublicリポジトリを見つけて、攻撃を仕掛ける攻撃者がいる

npmのTrusted PublishingとOIDC連携を使ってトークンレスでCIからnpmパッケージを公開する

npmは"Require two-factor authentication and disallow tokens"を設定する

Require two-factor authentication and disallow tokens
With this option, a maintainer must have two-factor authentication enabled for their account, and they must publish interactively. Maintainers will be required to enter 2FA credentials when they perform the publish. Automation tokens and granular access tokens cannot be used to publish packages.
https://docs.npmjs.com/requiring-2fa-for-package-publishing-and-settings-modification

MFAはフィッシング耐性の高いものを使う

  • SMS/TOTPはフィッシング耐性が低いので、できる限りフィッシング耐性の高いMFAを使う
  • WebAuthn/FIDO2などのセキュリティキー/Passkeyを使う
  • パスワード管理/MFA管理の戦略 | Web Scratch
  • OIDCなどでnpm token自体を減らせたので、自分の場合はnpmにはセキュリティキーのみ(+バックコードを保存)をMFAとして登録している
  • GitHubもセキュリティキーのみをMFAにしたいが、昔登録した Authenticator app が消せないというバグがある

Discussion