⚠️

サプライチェーン攻撃対策の記録

に公開
2025/12/26

npm packageを経由したマルウェア配布などが増加しており、2025年も大きなインシデント事例が複数報告されています。
そこでサプライチェーン攻撃への対策として行ったことを記録として残します。

参考記事

基本的に下記の記事を参考にさせていただいて対応を行いました。
https://zenn.dev/azu/articles/ad168118524135

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

■ 参考記事
https://docs.github.com/ja/actions/reference/security/secure-use
https://dev.classmethod.jp/articles/hardening-github-actions-with-pinact-commit-sha-semver/

SHA Pinする方法

pinactを使用して、ワークフローファイルのアクションバージョンをコミットSHAで固定します。
pinactはGitHub Actionsのワークフローにおける各種依存アクションのバージョンを固定してくれるCLIツールです。

■ pinact
https://github.com/suzuki-shunsuke/pinact

aquaインストール

まずpinactインストールにあたり、aquaをインストールして使用します。
aquaとはコマンドラインツールのバージョン管理ツールです。(CLI Version Manager)
Homebrewとは異なり、同じマシンで同じツールの複数バージョンを使い分けることができます。
またaqua-checksums.jsonによるチェックサムの管理・検証が可能です。
pinactはHomebrewでもインストールできますが、上記メリットを考慮しaquaを使用することにしました。

■ aqua
https://aquaproj.github.io/
https://github.com/aquaproj/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 インストール
https://github.com/suzuki-shunsuke/pinact/blob/a4788969833fc9831a93b192ef2a9bee4e93a286/INSTALL.md
https://tech.revcomm.co.jp/introduce-pinact-and-aqua

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)
https://zenn.dev/kou_pg_0131/articles/gha-static-checker

actionlint

主にworkflowとActionの構文系がチェック可能。

■ actionlint
https://github.com/rhysd/actionlint

■ 参考記事
https://rhysd.hatenablog.com/entry/2021/07/11/214313

actionlintをインストール

aquaでインストール

aqua g -i rhysd/actionlint
aqua i

チェック実行

コマンドで構文チェックを実行。エラーが表示された場合、指示に沿って修正を行う。

actionlint

ghalint

主にセキュリティ系のチェックが充実している。

■ ghalint
https://github.com/suzuki-shunsuke/ghalint/tree/main

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

■ 参考記事
https://zenn.dev/shunsuke_suzuki/articles/github-action-checkout-persist-credentials
https://github.com/suzuki-shunsuke/disable-checkout-persist-credentials

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

■ 参考記事
https://zenn.dev/shunsuke_suzuki/articles/github-actions-timeout-minutes
https://github.com/suzuki-shunsuke/ghatm

zizmor

主にセキュリティ系のチェックが充実している。

■zizmor
https://github.com/zizmorcore/zizmor
https://docs.zizmor.sh/

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
https://github.com/AikidoSec/safe-chain
■ socket npm & socket npx
https://docs.socket.dev/docs/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日間です。

  1. CLI Argument (highest priority):
npm install express --safe-chain-minimum-package-age-hours=72
  1. Environment Variable:
export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS=72
npm install express
  1. Config File (~/.aikido/config.json):
{
  "minimumPackageAgeHours": 72
}

https://github.com/AikidoSec/safe-chain?tab=readme-ov-file#minimum-package-age

注意事項

AikidoSec/safe-chainは既にインストール済みのnode_modulesに対してはスキャンできません。

ignore-scriptsでpreinstallスクリプトを実行させない

万一、悪意のあるコードを含んだpackageをインストールしてしまった場合でもpreinstallスクリプトを実行させないことで防御します。

  1. .npmrcを作成し、下記を追記する
    これでpackageはインストールされても悪意のあるスクリプトは実行されません。
ignore-scripts=true
  1. @lavamoat/allow-scriptsをインストール
    ライフサイクルスクリプトに依存するパッケージを使用する場合、@lavamoat/allow-scriptsを使用して特定のスクリプトのみ実行を許可するリストを作成します。
    globalにインストールできますが、プロジェクトごとにインストールする場合はこちら↓
npm install @lavamoat/allow-scripts
  1. npx --no-install allow-scripts setupで@lavamoat/preinstall-always-failpackageをインストール
    このパッケージは@lavamoat/allow-scriptsと組み合わせて使用します。npm installコマンドがライフサイクルスクリプトを実行しようとした場合に失敗するようフェイルセーフとして機能します。

  2. 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
    }
  }
}
  1. npm install、npm ci実行時に自動的にallow-scriptsを実行し、許可されたライフサイクルスクリプトのみを実行させる
    package.jsonのprepareにallow-scriptsを指定します。
"prepare": "allow-scripts"

■ OWASP Cheat Sheet Series - NPM Security best practices
https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html

■ @lavamoat/allow-scripts
https://github.com/LavaMoat/LavaMoat/tree/main/packages/allow-scripts

■ can-i-ignore-scripts
https://www.npmjs.com/package/can-i-ignore-scripts

OSV-Scannerで脆弱性検知を行う

OSV-ScannerはGoogleが運営するOSV(Open Source Vulnerabilities)データベースを使ったオープンソースの脆弱性スキャナーです。
npm auditはnpmのAdvisory DBのみを見ますが、OSV-ScannerはGitHub Advisory、NVD、その他複数のソースを統合したデータベースを使い、カバー範囲が広いです。

  1. brew install osv-scannerでインストール
  2. osv-scanner --lockfile package-lock.jsonでスキャン実行

■ OSV-Scanner
https://google.github.io/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
https://1password.com/jp

■ 参考記事
https://efcl.info/2023/01/31/remove-secret-from-local/
https://techblog.zozo.com/entry/1password-intro

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 を除外する機能がないため、全か無かの選択になる

■ 参考記事
https://zenn.dev/shunsuke_suzuki/articles/github-actions-enforce-sha-pinning

まとめ

以上、行うべきことは多いですがどれだけ対策しても十分ではなさそうです。
常にキャッチアップ&対応できるよう、継続して見直ししていきたいと思います。

Discussion