はじめに
この記事は 株式会社TRAILBLAZER Advent Calendar 2025 の記事です。
TRAILBLAZERでフロントエンドエンジニアをしている田原です。
2024年以降、Node.jsエコシステムにおけるサプライチェーン攻撃が急増しています。特にShai-Huludと呼ばれるマルウェアは、npmパッケージのpreinstallスクリプトを悪用し、開発者のマシンやCI環境に感染を広げており、2025年12月15日(現在)もHotな話題となっております。
本記事ではプロジェクトで実装した攻撃対策としての多層防御アプローチを解説していきます。
対象読者
- Node.js/npmを使用しているプロジェクトの開発者
- CI/CDパイプラインのセキュリティを強化したい方
- サプライチェーン攻撃の具体的な対策を知りたい方
サプライチェーン攻撃とは
サプライチェーン攻撃は、ソフトウェアの依存関係(サードパーティライブラリ)を通じて悪意のあるコードを混入させる攻撃手法です。
主な攻撃パターン
- 悪意あるパッケージの公開 - 人気パッケージに似た名前(typosquatting)で悪意あるコードを含むパッケージを公開
- 既存パッケージの乗っ取り - メンテナのアカウント侵害やソーシャルエンジニアリングによるパッケージ乗っ取り
- 依存関係の汚染 - 間接的な依存パッケージに悪意あるコードを混入
Shai-Huludの例
Shai-Huludマルウェアは、以下のような手法で感染を広げました:
{
"scripts": {
"preinstall": "node malicious-script.js"
}
}
npm installを実行するだけで、preinstallスクリプトが自動実行され
環境変数の窃取やバックドアの設置が行われます。
多層防御アプローチ
単一の対策では完全な防御は困難です。今回は以下の7層で防御を実装を試みております。
| 層 | 対策 | 防御タイミング | 目的 |
|---|---|---|---|
| 1 | save-exact=true |
npm install時 | バージョン固定 |
| 2 | ignore-scripts=true |
npm install時 | 悪意あるスクリプト実行阻止 |
| 3 | OSV-Scanner | CI | 既知の脆弱性検出 |
| 4 | gitleaks | CI | シークレット漏洩検出 |
| 5 | Semgrep | CI | 危険なコードパターン検出 |
| 6 | Aikido Safe Chain | CI | 24時間ルール・マルウェア検知 |
| 7 | e18e | CI | 依存関係の分析 |
ツール取得方法の方針
CIでセキュリティツールを取得する際、取得方法自体がサプライチェーン攻撃のリスクとなります。
curl | sh パターンは危険です
# ❌ 危険な例
curl -fsSL https://example.com/install.sh | sh
この方法は、外部URLが改ざんされた場合に悪意あるスクリプトが実行される可能性があります。
本実装では以下の優先順位でツールを取得しています:
| 優先度 | 取得方法 | 説明 |
|---|---|---|
| 1 | GitHub Action | バージョン固定でGitHubのセキュリティ審査を受けたActionsを使用 |
| 2 | GitHub CLI |
gh release downloadで公式リリースから取得(GitHub API経由で認証付き) |
| 3 | npmパッケージ | npmレジストリから取得 |
| 4 | curl(直接URL) | ❌ 禁止 |
各ツールの取得方法
| ツール | 取得方法 | 理由 |
|---|---|---|
| OSV-Scanner | GitHub CLI | 公式ActionはReusable Workflow形式で制約あり |
| gitleaks | GitHub CLI | 公式Actionは組織リポジトリで有料ライセンス必要 |
| Semgrep | GitHub Action | 公式提供あり |
| Aikido Safe Chain | npm パッケージ | 公式GitHub Action未提供 |
| e18e | npx | CLIツールとして提供 |
実装詳細
1. バージョン固定(save-exact=true)
なぜ必要か?
package.jsonでよく見る^や~は、セマンティックバージョニングに基づいて自動的に新しいバージョンをインストールする仕組みです。
{
"dependencies": {
"express": "^4.18.0"
}
}
この場合npm install時に4.19.0や4.20.0など、メジャーバージョンが同じ最新版がインストールされます。攻撃者がパッケージを乗っ取り、悪意あるコードを含む4.19.0をリリースした場合、自動的に感染する可能性が増加してしまいます。
設定方法
.npmrcファイルをプロジェクトルートに作成:
save-exact=true
既存プロジェクトへの適用
既存のpackage.jsonから^と~を削除する必要があります:
{
"dependencies": {
- "express": "^4.18.0",
- "axios": "~1.6.0"
+ "express": "4.18.0",
+ "axios": "1.6.0"
}
}
その後、npm installを実行してpackage-lock.jsonを更新します。
2. スクリプト実行阻止(ignore-scripts=true)
なぜ必要か?
npmパッケージはpreinstall、postinstallなどのライフサイクルスクリプトを定義できます。これらはインストール時に自動実行されるため、攻撃の起点となります。
設定方法
.npmrcに追加:
save-exact=true
ignore-scripts=true
注意点
ネイティブモジュール(node-gypを使用するパッケージなど)はビルドスクリプトが必要な場合があります。その場合は個別に対応が必要です。
# 特定のパッケージのみスクリプトを許可してインストール
npm install --ignore-scripts=false <package-name>
3. OSV-Scanner(脆弱性スキャン)
OSV-ScannerはGoogleが開発したオープンソースの脆弱性スキャナーです。
機能
-
package-lock.jsonをスキャンしてCVE脆弱性を検出 - OSV(Open Source Vulnerabilities)データベースを使用
GitHub Actionsでの実装
osv-scanner:
name: OSV-Scanner (脆弱性スキャン)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install OSV-Scanner via GitHub CLI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release download v2.0.2 --repo google/osv-scanner --pattern 'osv-scanner_linux_amd64'
chmod +x osv-scanner_linux_amd64
- name: Run OSV-Scanner
run: |
./osv-scanner_linux_amd64 --lockfile=package-lock.json --lockfile=setowa-web/package-lock.json --format=table
ポイント
-
GitHub CLIで公式リリースから取得:
gh release downloadはGitHub API経由で認証付きリクエストを行うため、curlより安全 -
--format=tableでCIログに見やすく出力 - 公式GitHub Action(
google/osv-scanner-action)はReusable Workflow形式で提供されており、他ジョブと同一ファイルで使用する際に制約があるため、CLI版を使用
4. gitleaks(シークレット検出)
gitleaksは、Git履歴からAPIキーやパスワードなどの機密情報を検出するツールです。
GitHub Actionsでの実装
gitleaks:
name: gitleaks (シークレット検出)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install gitleaks via GitHub CLI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release download v8.21.2 --repo gitleaks/gitleaks --pattern 'gitleaks_8.21.2_linux_x64.tar.gz'
tar -xzf gitleaks_8.21.2_linux_x64.tar.gz
chmod +x gitleaks
- name: Run gitleaks
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
./gitleaks detect --source . --log-opts="origin/${{ github.base_ref }}..HEAD" --verbose
else
./gitleaks detect --source . --verbose
fi
ポイント
-
fetch-depth: 0でGit履歴全体を取得 - PR時は変更コミットのみ、push時はリポジトリ全体をスキャン
-
公式GitHub Action(
gitleaks/gitleaks-action)は組織リポジトリで有料ライセンスが必要なため、GitHub CLI経由でCLI版を使用
5. Semgrep(静的解析)
Semgrepは、コードパターンを検出する静的解析ツールです。XSSやSQLインジェクション、eval()の使用など、セキュリティ上問題のあるコードを検出できます。
GitHub Actionsでの実装
semgrep:
name: Semgrep (静的解析)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: p/typescript p/javascript p/security-audit
publishToken: ''
env:
SEMGREP_RULES: p/typescript p/javascript p/security-audit
使用ルールセット
-
p/typescript- TypeScript固有のルール -
p/javascript- JavaScript固有のルール -
p/security-audit- セキュリティ監査用ルール
ポイント
-
公式GitHub Action(
semgrep/semgrep-action)を使用: 問題なく利用可能 -
publishToken: ''でSemgrep Cloudへの結果送信を無効化
6. Aikido Safe Chain(マルウェア検知)
Aikido Safe Chainは、npmパッケージのマルウェア検知に特化したツールです。
主な機能
- 24時間ルール: 公開から24時間以内のパッケージをブロック(新規公開パッケージは悪意ある可能性が高い)
- マルウェア検知: Aikido Intelデータベースでリアルタイム検証
GitHub Actionsでの実装
aikido-safe-chain:
name: Aikido Safe Chain (マルウェア検知)
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./setowa-web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Install Aikido Safe Chain via npm
run: npm install -g @aikidosec/safe-chain
- name: Install dependencies with Safe Chain
run: safe-chain npm ci
ポイント
-
npmパッケージとしてインストール: 公式GitHub Actionが提供されていないため、
@aikidosec/safe-chainをnpm経由でインストール -
safe-chain npm ciで依存パッケージを検証しながらインストール
7. e18e(依存関係分析)
e18eは、依存関係の分析ツールです。
node_modulesの肥大化などについて検知を行います。
GitHub Actionsでの実装
e18e:
name: e18e (依存関係分析)
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./setowa-web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Run e18e analyze
run: npx @e18e/cli analyze
完成したワークフロー
すべてを統合した.github/workflows/security.yml:
name: security
on:
push:
branches:
- main
pull_request:
jobs:
osv-scanner:
name: OSV-Scanner (脆弱性スキャン)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install OSV-Scanner via GitHub CLI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release download v2.0.2 --repo google/osv-scanner --pattern 'osv-scanner_linux_amd64'
chmod +x osv-scanner_linux_amd64
- name: Run OSV-Scanner
run: |
./osv-scanner_linux_amd64 --lockfile=package-lock.json --lockfile=setowa-web/package-lock.json --format=table
gitleaks:
name: gitleaks (シークレット検出)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install gitleaks via GitHub CLI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release download v8.21.2 --repo gitleaks/gitleaks --pattern 'gitleaks_8.21.2_linux_x64.tar.gz'
tar -xzf gitleaks_8.21.2_linux_x64.tar.gz
chmod +x gitleaks
- name: Run gitleaks
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
./gitleaks detect --source . --log-opts="origin/${{ github.base_ref }}..HEAD" --verbose
else
./gitleaks detect --source . --verbose
fi
semgrep:
name: Semgrep (静的解析)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: p/typescript p/javascript p/security-audit
publishToken: ''
env:
SEMGREP_RULES: p/typescript p/javascript p/security-audit
aikido-safe-chain:
name: Aikido Safe Chain (マルウェア検知)
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./setowa-web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Install Aikido Safe Chain via npm
run: npm install -g @aikidosec/safe-chain
- name: Install dependencies with Safe Chain
run: safe-chain npm ci
e18e:
name: e18e (依存関係分析)
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./setowa-web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Run e18e analyze
run: npx @e18e/cli analyze
運用上の注意点
検出時のCI失敗設定
導入初期は検出してもCIを失敗させない設定にすることを推奨します。これにより
- まず検出結果を確認
- 誤検知の調整
- 段階的にCI失敗設定を有効化
という流れで運用できます。
まとめ
サプライチェーン攻撃への対策は、単一のツールでは不十分です。本記事で紹介した多層防御アプローチにより、以下の防御を行っています。
| 攻撃パターン | 対策 |
|---|---|
| 意図しないバージョン更新 | save-exact=true |
| 悪意あるインストールスクリプト | ignore-scripts=true |
| 既知の脆弱性 | OSV-Scanner |
| シークレット漏洩 | gitleaks |
| 危険なコードパターン | Semgrep |
| 新規公開パッケージ・マルウェア | Aikido Safe Chain |
| 依存関係の可視化 | e18e |
これらの対策を組み合わせることで、サプライチェーン攻撃のリスクを大幅に軽減できます。
補足
ニュースになってからKyouhei Fukuda氏の以下の記事で内容と防御アプローチについてキャッチアップを行いました。(なので幾つかは以下記事にも書かれていることです)
ありがとうございました🙇
また、以下の記事(なぎ氏)の内容についても参考にさせて頂きました。
ありがとうございました🙇
最後に
本記事を最後まで読んで頂きありがとうございます![]()
TRAILBLAZERでは一緒に働くメンバーを募集中です!!
皆さまからのご連絡お待ちしております![]()
参考資料