サプライチェーン攻撃対策の記録
npm packageを経由したマルウェア配布などが増加しており、2025年も大きなインシデント事例が複数報告されています。
そこでサプライチェーン攻撃への対策として行ったことを記録として残します。
参考記事
基本的に下記の記事を参考にさせていただいて対応を行いました。
packageのversion固定
- package.jsonに記載のあるpackage versionを固定にします。(^ハットは使用しない)
.npmrcに下記を設定しておくとインストール時versionを固定してくれます。save-exact=true - package-lock.jsonを使用し、
npm iではなくnpm ciでlockファイルに固定されたversionをインストールします。
GitHub ActionsでSHA Pinを行う
SHA Pinとは
SHA pinningとは特定のGitHub Actionsのリリース(コミット)をハッシュ値(SHA)で固定することです。
これを行うことで、万一タグ(例: v5.0.0)のコードが悪意のあるコードに差し替えられた場合でもワークフローでそのコードを実行してしまうリスクを防ぎます。
例:タグ指定のみ
uses: actions/checkout@v5.0.0
例:SHA Pin
uses: actions/checkout@d5er54gsddget8s3 # v5.0.0
■ 参考記事
SHA Pinする方法
pinactを使用して、ワークフローファイルのアクションバージョンをコミットSHAで固定します。
pinactはGitHub Actionsのワークフローにおける各種依存アクションのバージョンを固定してくれるCLIツールです。
■ pinact
aquaインストール
まずpinactインストールにあたり、aquaをインストールして使用します。
aquaとはコマンドラインツールのバージョン管理ツールです。(CLI Version Manager)
Homebrewとは異なり、同じマシンで同じツールの複数バージョンを使い分けることができます。
またaqua-checksums.jsonによるチェックサムの管理・検証が可能です。
pinactはHomebrewでもインストールできますが、上記メリットを考慮しaquaを使用することにしました。
■ aqua
aquaのインストールはHomebrewにて行います。
brew install aqua
aqua -v
aquaの初期化(aqua.yamlの生成)
aqua init
aqua.yamlでchecksumを有効化します。
checksum:
enabled: true
require_checksum: true
pinactインストール
パッケージ(pinact)を追加
aqua g -i suzuki-shunsuke/pinact
aqua.yamlにパッケージの定義が追加されます。
packages:
- name: suzuki-shunsuke/pinact@v3.4.2
pinactをインストール
aqua i
下記コマンドでaqua.yamlで定義されている各種パッケージをインストールします。
aqua-checksums.jsonを更新
aqua update-checksum
aqua.yamlでチェックサムの検証を有効化している場合、依存パッケージの追加や更新などを行なった際は更新コマンドを先に実行し、追加の場合はaqua iでインストールを行います。
■ aqua・pinact インストール
pinactでSHA Pinを行う
pinact run
pinactを実行しSHA Pinする
GitHub Actionsに必要な権限のみを付与
ワークフローにpermissionsを設定し、contents: readだけを付与します。
GitHub Actions を静的検査する
GitHub Actionsの構文やセキュリティチェックのために、ツールをインストールします。
ツールでチェックを行う理由は下記です。
- GitHub Actions サービス側のワークフローのチェックは緩いため。(例えば想定していないキーがあっても単に無視されるためキー名のタイポに気付けない)
- Workflowを手元で確認できるようにするため。
■GitHub Actions を静的検査するツールの紹介 (actionlint/ghalint/zizmor)
actionlint
主にworkflowとActionの構文系がチェック可能。
■ actionlint
■ 参考記事
actionlintをインストール
aquaでインストール
aqua g -i rhysd/actionlint
aqua i
チェック実行
コマンドで構文チェックを実行。エラーが表示された場合、指示に沿って修正を行う。
actionlint
ghalint
主にセキュリティ系のチェックが充実している。
■ ghalint
ghalintをインストール
aquaでインストール
aqua g -i suzuki-shunsuke/ghalint
aqua i
チェック実行
コマンドで構文チェックを実行。エラーが表示された場合、指示に沿って修正を行う。
Workflowをチェック
.github/workflows/ ディレクトリ内の Workflow ファイルをチェックする
ghalint run
Actionsをチェック
action.yamlをチェックする
ghalint run-action
自動修正
WorkflowとActionsにて大量のエラーが検出された場合、自動修正ツールで対応します。
persist-credentials: falseが設定されていないためのエラー
GitHub Actions の actions/checkout の input persist-credentials は、リポジトリをチェックアウトしたあとに GitHub Access Token や SSH key などの認証情報を Git の config に残すか否かの設定です。デフォルトでは true で job の終了時まで Git の config に保存されます。false だと actions/checkout の終了時に Git の config から消されます。
aquaでインストール
aqua g -i suzuki-shunsuke/disable-checkout-persist-credentials
aqua i
引数無しで実行すると .github/workflows 配下の workflow が修正されます。
disable-checkout-persist-credentials
パスを指定する場合はこちら↓
disable-checkout-persist-credentials foo/action.yaml
■ 参考記事
timeout-minutesが設定されていないためのエラー
timeout-minutesを設定しておくと、万一GitHub Actionsが失敗した場合に無駄なリソースを消費しなくて済みます。
aquaでインストール
aqua g -i suzuki-shunsuke/ghatm
aqua i
workflow があるリポジトリのルートディレクトリでghatm setを実行すると、デフォルトでtimeout-minutes: 30が追記されます。
ghatm set
引数指定の場合はこちら↓
ghatm set -t 60
■ 参考記事
zizmor
主にセキュリティ系のチェックが充実している。
■zizmor
zizmorをインストール
Homebrewでインストール
brew install zizmor
チェック実行
カレントディレクトリ内の全ての Workflow/Action をチェック
zizmor .
複数指定する場合はこちら↓
zizmor foo.yml bar/baz.yml
npm、npxコマンドはスキャン後に実行
インストールするpackageに悪意のあるコードが含まれていないかをスキャンしてからインストールするために、AikidoSec/safe-chainを使用します。
AikidoSec/safe-chainはnpm cli、npx、yarn、pnpm、pnpx、bun、bunxなど既存のコマンドをラップする形で保護し、新規パッケージインストール前にスキャンチェックします。パッケージにマルウェアが含まれていることを検知すると終了を促し、ダウンロードまたは実行するのを防止します。
※同様のツールでsocket npm & socket npxもあります。
■ AikidoSec/safe-chain ■ socket npm & socket npx
AikidoSec/safe-chainのインストール
グローバルインストール
npm install -g @aikidosec/safe-chain
初期設定
safe-chain setup
ターミナル再起動後、インストール・有効化を確認
npm install safe-chain-test
Aikido Safe Chainがこのパッケージをマルウェアとしてフラグ付けしているため、有効化できていればインストールがブロックされます。
以降はalias登録など行わなくても、npm npxが保護された状態で使用できます。
Aikido Safe Chainのメリット
Aikido Intelが数百万のOSSパッケージを監視して、公開から24時間未満のパッケージバージョンをインストールしないようブロックしてくれます。ブロックする期間を長く設定することも可能です。
■ Minimum Package Ageを設定する
下記いずれかの方法で設定します。例は3日間です。
- CLI Argument (highest priority):
npm install express --safe-chain-minimum-package-age-hours=72
- Environment Variable:
export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS=72
npm install express
- Config File (~/.aikido/config.json):
{
"minimumPackageAgeHours": 72
}
注意事項
AikidoSec/safe-chainは既にインストール済みのnode_modulesに対してはスキャンできません。
ignore-scriptsでpreinstallスクリプトを実行させない
万一、悪意のあるコードを含んだpackageをインストールしてしまった場合でもpreinstallスクリプトを実行させないことで防御します。
-
.npmrcを作成し、下記を追記する
これでpackageはインストールされても悪意のあるスクリプトは実行されません。
ignore-scripts=true
-
@lavamoat/allow-scriptsをインストール
ライフサイクルスクリプトに依存するパッケージを使用する場合、@lavamoat/allow-scriptsを使用して特定のスクリプトのみ実行を許可するリストを作成します。
globalにインストールできますが、プロジェクトごとにインストールする場合はこちら↓
npm install @lavamoat/allow-scripts
-
npx --no-install allow-scripts setupで@lavamoat/preinstall-always-failpackageをインストール
このパッケージは@lavamoat/allow-scriptsと組み合わせて使用します。npm installコマンドがライフサイクルスクリプトを実行しようとした場合に失敗するようフェイルセーフとして機能します。 -
npx --no-install allow-scripts autoを実行
上記コマンド実行で自動的にpackage.jsonにallowScriptsのリスト項目が追記されます。
true/falseは各自で編集しますが、trueにする必要があるかどうかを確認するためにcan-i-ignore-scriptsを使用します。(ただし全部判定できるわけではありません)
can-i-ignore-scriptsを事前にインストールして実行するか、npx can-i-ignore-scriptsで確認します。
{
"lavamoat": {
"allowScripts": {
"keccak": true,
"core-js": false
}
}
}
- npm install、npm ci実行時に自動的に
allow-scriptsを実行し、許可されたライフサイクルスクリプトのみを実行させる
package.jsonのprepareにallow-scriptsを指定します。
"prepare": "allow-scripts"
■ OWASP Cheat Sheet Series - NPM Security best practices
■ @lavamoat/allow-scripts
■ can-i-ignore-scripts
OSV-Scannerで脆弱性検知を行う
OSV-ScannerはGoogleが運営するOSV(Open Source Vulnerabilities)データベースを使ったオープンソースの脆弱性スキャナーです。
npm auditはnpmのAdvisory DBのみを見ますが、OSV-ScannerはGitHub Advisory、NVD、その他複数のソースを統合したデータベースを使い、カバー範囲が広いです。
-
brew install osv-scannerでインストール -
osv-scanner --lockfile package-lock.jsonでスキャン実行
■ OSV-Scanner
今後行いたいこと
pnpmコマンドへの移行
pnpmは下記のメリットがあり今回移行を検討しましたが、稀にpnpmに変更することによって今まで通り使用できないpackageがあるかもしれないとのことで、すでにnpmコマンド使用中のプロジェクトで移行は見送ることにしました。
次回新規プロジェクトの際にpnpm使用にしようと思います。
■ pnpmのメリット
- pnpm 10では、Lifecycle Scriptsがデフォルトでは実行されない。
※npmで配布されるマルウェアはpostinstallでコードを実行して、機密情報を盗み出すといった攻撃を行う。 -
minimumReleaseAgeでpackageの最新verが公開されてから pnpm がそれをインストールするまでに経過しなければならない最小分数を定義できる。誤って最新verリリース直後にインストールしてしまうことを防げる。
https://pnpm.io/ja/settings#minimumreleaseage
ローカルの環境変数・パスワードなどの削除
ローカル環境がマルウェア汚染された場合、.envファイルの環境変数・パスワードが流出する可能性があります。
1Paswordなどを使用して一括管理しローカルからは削除するのが望ましいです。
■1Password
■ 参考記事
GitHubリポジトリの"Require actions to be pinned to a full-length commit SHA”を有効にする
Workflow、ActionのymlファイルにてSHA指定に書き換えてコミットしている場合、GitHub Actionsはその特定のコミットだけを参照します。
リポジトリでRequire actions to be pinned to a full-length commit SHAチェックを入れ有効化すると、誤って uses: actions/checkout@v5 のように書いてPRしても、GitHubがリポジトリルールとしてWorkflowを失敗させるようにします。
リポジトリレベルでenforce(強制)できるのは強力ですが下記のデメリットもあるそうです。
- 実行時エラー: 事前に問題を検出できないため、PR マージ後に初めて問題が発覚する可能性
- 依存 action の影響: 自分が適切に SHA pinning していても、依存先が対応していないと動作しない
- 移行の困難さ: 多くの既存 action が SHA pinning に対応していないため、導入のハードルが高い
- 除外設定の不在: 特定の action を除外する機能がないため、全か無かの選択になる
■ 参考記事
まとめ
以上、行うべきことは多いですがどれだけ対策しても十分ではなさそうです。
常にキャッチアップ&対応できるよう、継続して見直ししていきたいと思います。
Discussion