Nx s1ngularity 2025年 AIのCLIに秘密を探させた悪い版
PRの題名をそのままシェルに埋め込むワークフローが、書き込みの権限を持つトークンで動いていた。そこから公開用のワークフローを動かされ、npm のトークンを取られた。出された悪い版は、導入時のスクリプトで、開発者の手元にあるAIのCLIにファイルを探させ、集めた資格情報を本人の GitHub に公開のリポジトリとして上げた。
要点
- 入口は、PRの題名をシェルの文字列に直に埋め込む検査のワークフローだった。
pull_request_targetで動くため、外の者のPRでも読み書きのできるGITHUB_TOKENを持っていた。 - 検査のワークフロー自体は秘密を持たない。そのトークンで、npm のトークンを持つ公開用のワークフローを、書き換えたブランチの上で動かされた。
- 直したはずのワークフローは、古いブランチに残っていた。
pull_request_targetは、PRの向け先のブランチにある定義で動く。 - 悪い版の導入時のスクリプトは、手元のAIのCLIにファイルの一覧を作らせ、資格情報を集めて、本人のアカウントに
s1ngularity-repositoryを含む名前の公開のリポジトリを作って上げた。 - 根本の守りは、公開に長く使うトークンを置かない、外の入力をシェルに埋め込まない、ワークフローの権限を読み取りにする、手元に平文の鍵を置かない、の4つ。
何が起きたか
| 日時 | 出来事 |
|---|---|
| 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]。
通った道筋
| 条件 | 何があったか |
|---|---|
| 題名の埋め込み | 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]。 |
悪い版が手元でしたこと
- 導入時のスクリプトは、ファイルを走査してテキストのファイルの道と資格情報を集め、符号化して、本人の 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に見つけられて困る鍵を手元に平文で置かないことにある。
どこで止められたか
| 止める所 | この件で | 限り |
|---|---|---|
| 外の入力をシェルに埋め込まない | 題名を 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年 で続けて扱う。
参照
- Malicious versions of Nx and some supporting plugins were published GHSA-cxm3-wv7p-598c Nx、GitHub Security Advisory、2025年8月27日
- S1ngularity - What Happened, How We Responded, What We Learned Nx、2025年9月5日
- Secure use reference GitHub Docs
- Trusted publishing for npm packages npm Docs