Nx s1ngularity 2025年 AIのCLIに秘密を探させた悪い版

PRの題名をそのままシェルに埋め込むワークフローが、書き込みの権限を持つトークンで動いていた。そこから公開用のワークフローを動かされ、npm のトークンを取られた。出された悪い版は、導入時のスクリプトで、開発者の手元にあるAIのCLIにファイルを探させ、集めた資格情報を本人の GitHub に公開のリポジトリとして上げた。

GitHub の告知 GHSA-cxm3-wv7p-598c、深刻度は Critical、CVE は無い[1]。影響は nx 21.5.0 から 21.8.0、20.9.0 から 20.12.0 と、@nx の一部の包み[1]。悪い版は約4時間公開され、npm が削除し、Nx の公開用のトークンを全部失効させた[2]。Nx は公開を npm の Trusted Publishing に移し、トークンでは公開できないようにした[1]。時刻は米国東部夏時間。2025年9月5日の Nx の事後報告までの公表で書いています。告知は、他の秘密が悪用されたかの確かめを続けていると書き、2026年11月時点で完了は公表されていない[1]。

要点

  • 入口は、PRの題名をシェルの文字列に直に埋め込む検査のワークフローだった。pull_request_target で動くため、外の者のPRでも読み書きのできる GITHUB_TOKEN を持っていた。
  • 検査のワークフロー自体は秘密を持たない。そのトークンで、npm のトークンを持つ公開用のワークフローを、書き換えたブランチの上で動かされた。
  • 直したはずのワークフローは、古いブランチに残っていた。pull_request_target は、PRの向け先のブランチにある定義で動く。
  • 悪い版の導入時のスクリプトは、手元のAIのCLIにファイルの一覧を作らせ、資格情報を集めて、本人のアカウントに s1ngularity-repository を含む名前の公開のリポジトリを作って上げた。
  • 根本の守りは、公開に長く使うトークンを置かない、外の入力をシェルに埋め込まない、ワークフローの権限を読み取りにする、手元に平文の鍵を置かない、の4つ。

何が起きたか

8月21日に弱いワークフローが入り、24日にトークンを取られ、26日に悪い版が出て約4時間で消され、27日に告知と手当てが進んだ 1 弱点が入る 8月21日 題名の検査の ワークフロー 2 鍵を取られる 8月24日 古いブランチから npm のトークン 3 悪い版 8月26日 18時32分から 22時44分に削除 4 手当て 8月27日 告知、トークン廃止 古いブランチを修正 1から2まで3日。指摘を受けて直したのは master だけだった 時刻は米国東部夏時間
図1 弱点が入ってから手当てまでの7日
日時出来事
2025年8月21日 16時31分PRの題名を検査するワークフローを入れるPRを統合する。任意の bash の命令を走らせられる注入の弱点があった[1]
8月21日 22時48分X に、このワークフローに注入の弱点があるという投稿が出る[1]
8月22日 15時45分投稿に気付いたチームが、ワークフローを master から戻す。この時はこれで使えなくなると考えていた[1]
8月24日 16時50分から17時11分攻撃者が、npm のトークンを外の webhook へ送る変更のコミットを作る。そのコミットでPRが作られて検査のワークフローが動き、続いて公開用のワークフローが動く。PRと実行の記録は消されており、後で監査のログから分かった[1]
8月26日 18時32分から20時37分nx と @nx の包みの悪い版が次々に公開される[1]
8月26日 20時30分利用者が GitHub の issue で知らせる[1]
8月26日 22時44分npm が悪い版を削除し、公開のトークンを無効にする。23時57分に公開の権限のあるトークンを全部失効させる[1]
8月27日 1時53分告知 GHSA-cxm3-wv7p-598c を出す[1]
8月27日 3時33分Nx を使っていない人も被害を受けた報告から、エディタの拡張の Nx Console が最新の nx を入れていたと分かる。8時33分に、入れない版の拡張を出す[1]
8月27日 11時57分Nx の全部の包みを、2要素認証を必須にしトークンで公開できない設定にし、Trusted Publishing へ移す[1]
8月27日 13時55分から15時28分不審なコミットと数日前のワークフローの実行を結び付け、古いブランチが残りの入口だったと再現する。弱いワークフローを含むブランチを全部更新し、外の協力者のPRで動くワークフローに承認を求める[1]
9月5日Nx が事後報告を出す[2]
  • Nx は、悪い版に npm の出どころの証明が付いていないことから、自分たちのCIで作られた物ではないと判断した[2]。出どころの証明は、付いていない版を入れるのを止めない。
  • Nx の週の導入数は約600万回と Nx は書く[2]。
  • Nx Console の 18.6.30 から 18.65.1 は、版を確かめるために最新の nx を入れていた。悪い版が最新だった間にエディタを開いただけで、導入時のスクリプトが走り得た[1]。

通った道筋

題名を埋め込む検査のワークフローが読み書きのトークンを持ち、そのトークンで公開用のワークフローを書き換えたブランチの上で動かし、npm のトークンを外へ送らせ、悪い版を公開した Nx のリポジトリの GitHub Actions 外の者のPR 古いブランチへ向ける 題名に命令 題名の検査 pull_request_target 題名を bash に埋め込む GITHUB_TOKEN 既定のまま 読み書きの権限 ブランチを作る CIのスクリプトを差し替え API で公開を起動 publish.yml workflow_dispatch NPM_TOKEN を持つ 外へ送る webhook 盗んだトークンで公開 CIを通らない。出どころの証明なし postinstall の付いた nx の版 記録を消すため、作ったブランチと ワークフローの実行を消した master はブランチの保護で書けなかった
図2 検査のワークフローから npm のトークンまで
条件何があったか
題名の埋め込みrun: の中で ${{ github.event.pull_request.title }} を echo していた。式は実行の前に文字列として展開されるので、題名に書いた命令がそのまま走る[1]。
pull_request_targetこの起動の仕方は、PRの向け先のリポジトリで、読み書きの GITHUB_TOKEN を持って動く。定義は向け先のブランチにある物が使われ、master とは限らない。master から戻しても、弱い定義が残る古いブランチへPRを向ければ動いた[1]。
読み書きの既定リポジトリの設定は、ワークフローのトークンの既定が読み書きのままだった。GitHub は2023年2月に新しいリポジトリの既定を読み取りに変えたが、既にあるリポジトリは変わらなかった[2]。
公開の起動publish.yml に workflow_dispatch があり、API から起動できた。攻撃者は、CIのスクリプトを差し替えたブランチを作り、その上で公開用のワークフローを動かし、npm のトークンを外へ送らせた[2]。秘密はフォークからは読めない設定だったが、この起動は Nx のリポジトリの上で起きた[1]。
長く使うトークン公開はリポジトリの秘密に置いた npm のトークンで行っていた。Nx は、信頼する環境に入れば npm へ公開できた形を、ただ1つの決定的な誤りと書く[2]。

悪い版が手元でしたこと

導入時のスクリプトが走り、手元のAIのCLIにファイルの一覧を作らせ、資格情報を集め、gh の資格情報で本人の公開のリポジトリへ上げ、シェルの設定に停止の命令を足した 入れる道 npm install 推移的な依存 エディタの拡張 postinstall 開発者の権限で動く telemetry.js 手元のAIのCLI Claude、Gemini など テキストのファイルの 一覧を作らせる 集める ファイルの道と資格情報 /tmp/inventory.txt 本人の GitHub へ gh の資格情報で 公開のリポジトリを作る シェルの設定 .bashrc と .zshrc に 停止の命令を足す 集めた物は符号化し、s1ngularity-repository を含む名前のリポジトリに上げた
図3 悪い版の導入時のスクリプトがしたこと
  • 導入時のスクリプトは、ファイルを走査してテキストのファイルの道と資格情報を集め、符号化して、本人の GitHub のアカウントに s1ngularity-repository を含む名前のリポジトリを作って上げた[1]。上げるのには GitHub の CLI を使った[2]。
  • Nx は、スクリプトが Claude や Gemini のような手元のAIの道具を使おうとしたと書く[2]。告知に載ったスクリプトの指示文は、AIに、テキストの設定や環境の定義のファイルを探し、中身を読まずに道だけを /tmp/inventory.txt に書くよう求めていた。例として *.env や *.conf を挙げる[1]。
  • .zshrc と .bashrc に sudo shutdown -h 0 を足した。端末を開くとパスワードを求め、入れると機械が止まる[1]。
  • GitHub はこれらのリポジトリを止めるか、検索に出ないよう非公開にし始めた。告知は、一度は公開だった可能性が高く、中の資格情報は漏れたと考えるよう書く[1]。
公表に無いこと

多くの解説は、AIのCLIを確認の仕組みを外す指定で起動した、影響を受けたアカウントやリポジトリが何千ある、と書く。Nx の告知と事後報告には、CLIの起動の指定も、影響の数も無い[1][2]。

この件で手元のAIのCLIは、悪いコードの代わりにファイルを探す道具として使われた。開発者の権限で動くスクリプトは、開発者が使えるAIの道具も同じように使える。AIの道具の権限の設定は、AIに頼む作業を絞る物で、同じ機械で動く悪いコードを止める物ではない。守りは、AIに見つけられて困る鍵を手元に平文で置かないことにある。

どこで止められたか

ワークフローの書き方、トークンの権限、公開の仕組み、取り込む側、手元の鍵の5つの段に止める所を置く ワークフローの書き方 トークンの権限 公開の仕組み 取り込む側 手元の鍵 1 外の入力をシェルに埋め込まない 環境変数で渡す。直すときは全ブランチ 2 トークンは読み取りを既定に 要るジョブにだけ書き込みを足す 3 公開に長く使うトークンを置かない Trusted Publishing と人の承認 4 出たばかりの版と導入時のスクリプト 待つ日数を置き、スクリプトを切る 5 平文の鍵を手元に置かない 見つかっても使えない形にしておく
図4 段ごとの止める所
止める所この件で限り
外の入力をシェルに埋め込まない題名を run: に直に展開していた[1]。GitHub は、式の値を中間の環境変数に入れて渡すよう求める[3]master だけ直しても、古いブランチの定義は pull_request_target で動く。この件はそこを通った[1]
pull_request_target を避けるGitHub は、要らなければこの起動を使わないよう求める[3]。題名の検査に書き込みの権限は要らない使うなら、外のコードを取り出さず、権限を最小にする
トークンは読み取りを既定に既定が読み書きのままだった[2]。読み取りなら、ブランチを作ることも公開を起動することもできない既にあるリポジトリは自動では変わらない。設定を見に行く
公開に長く使うトークンを置かないリポジトリの秘密の npm のトークン1つで、CIを通らずに公開できた[2]Trusted Publishing でも、公開用のワークフローを書き換えて動かされれば公開される。人の承認を挟む
出どころの証明を確かめる悪い版には証明が無く、Nx はそれで自分の物ではないと分かった[2]npm は証明の無い版も入れる。確かめる側が拒まない限り効かない
待つ日数と導入時のスクリプト悪い版は約4時間で消えた[2]。導入時のスクリプトで動いた[1]Nx Console のように、人の知らない所で入れる道がある[1]

守り方

根本の直し方
  • npm への公開から長く使うトークンを無くし、OIDC で入る Trusted Publishing にする。設定の後は、包みの公開の設定で、2要素認証を求めてトークンを拒む形にする[4]。Nx はこれに加え、CIからの公開に人の2要素認証の承認を求める形にした[2]。
  • PRの題名、本文、ブランチの名前のような外から来る値を、run: の中で ${{ }} として展開しない。環境変数に入れて、シェルの変数として引用して使う[3]。
  • ワークフローの GITHUB_TOKEN は、リポジトリの既定を読み取りにし、要るジョブにだけ要る権限を書く[3]。
  • pull_request_target を要らない所で使わない[3]。弱いワークフローを直すときは、全部のブランチから消す。外の協力者のPRで動くワークフローには承認を求める[1]。
  • 手元に平文の鍵を置かない。鍵はリポジトリの外に置き、用途ごとに権限を絞り、期限の短い物にする。置き場所と拒否の設定は秘密の鍵の扱いにある。

悪い例 架空のリポジトリの題名の検査

on:
  pull_request_target:
    types: [opened, edited, synchronize, reopened]
jobs:
  title:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Checking: ${{ github.event.pull_request.title }}"   # 題名がシェルの文になる

直した例 起動を分け、権限を読み取りにし、値を環境変数で渡す

on:
  pull_request:                      # 外のPRには秘密も書き込みの権限も渡らない
    types: [opened, edited, synchronize, reopened]
permissions:
  contents: read
jobs:
  title:
    runs-on: ubuntu-latest
    steps:
      - env:
          TITLE: ${{ github.event.pull_request.title }}
        run: printf 'Checking: %s\n' "$TITLE"   # 値として扱われ、命令として読まれない
緩和の策
  • 公開から日の浅い版を入れない。pnpm の minimumReleaseAge は既定で1440分待つ。約4時間で消えたこの版は、この設定なら入らなかった。導入時のスクリプトも許した依存だけに絞る。設定はAIが持ち込む部品の危険にある。
  • エディタの拡張、AIの道具、スクリプトが、人の知らない所で部品を入れる道を棚卸しする。Nx は、拡張が最新の版を入れていたことを被害を広げた点に挙げる[1]。
  • CIに CodeQL のような点検を入れ、ワークフローへの注入を統合の前に拾う。Nx は8月22日に有効にした[1]。
  • 被害を確かめるときは、/tmp/inventory.txt の有無、GitHub の監査のログで s1ngularity-repository を含むリポジトリの作成、シェルの設定に足された行を見る。見つかれば npm、GitHub、環境変数にあった鍵を全部差し替える。GitHub の CLI の鍵は、連携の許可を取り消して作り直す[1]。
  • 社内の登録所や中継に悪い版が残っていれば消す[1]。

型としての整理は A03 ソフトウェアサプライチェーンの不備 の型2と型3に当たる。導入時のスクリプトで秘密を集めて広がる型は、翌月の npm 2025年 で続けて扱う。

参照

  1. Malicious versions of Nx and some supporting plugins were published GHSA-cxm3-wv7p-598c Nx、GitHub Security Advisory、2025年8月27日
  2. S1ngularity - What Happened, How We Responded, What We Learned Nx、2025年9月5日
  3. Secure use reference GitHub Docs
  4. Trusted publishing for npm packages npm Docs