手がかりはタイムスタンプだった
npmは2026年7月8日、16年で最大のセキュリティ変更をリリースした——インストールスクリプトを既定で無効化。その1週間以内に、2件の別々のnpm侵害が、ペイロードをパッケージのインストール時ではなくインポート時に発火するよう移し、新しい既定値も--ignore-scriptsもまっすぐ迂回した。エコシステムがちょうど1年かけて監視を学んだ仕組みは、すでに移動していた。移動しなかった唯一のものは、悪意あるバージョンの新しさ——そしてそれこそがBromureの経過日数ゲートが確認するシグナルだ。
エコシステム全体が1年をかけて、npmのインストールスクリプトこそが危険だと 学んだ。7月8日、npmはそれを既定で無効にした。その3日後、ある侵害はペイロード をインストールスクリプトの外へ移した。さらに3日後、2件目はインストール スクリプトをまったく持たずに現れた。どちらもパッケージをインポートした ときに発火する。修正と回避は同じ週に交差した。
ある開発者が7月14日にプロジェクトへ@asyncapi/generatorを追加し、npm installを実行する。そのパッケージにはpreinstallもinstallも
postinstallフックもない——スクリプトスキャナーが印を付けるものは何もなく、
npmの真新しい既定値がスキップすべきものも何もない。インストールはきれいに
終わる。コードが初めてrequire('@asyncapi/generator')を実行したその瞬間、
パッケージ自身のエントリーポイントに組み込まれたローダーが分離したNode
プロセスを起動し、8.2 MBの第二段階をIPFS経由で取得して復号し、認証情報を
探し始める。インストールに関して何もおかしく見えなかったのは、おかしかった
部分がインストール時に起きなかったからだ。
昨年の攻撃に向けて出された修正
ほぼ1年のあいだ、npmサプライチェーン攻撃の物語は同じだった——侵害された
パッケージがpostinstallフックを抱え、npm installがあなたのマシンでそれを
自動実行する。そこで7月8日、npmはバージョン12
をリリースした——このツールの16年で最大のセキュリティ再設計だ。依存関係の
ライフサイクルスクリプトは既定で無効になった。「依存関係のpreinstall、install、
postinstallスクリプトは、許可しない限り実行されない」。Git依存やリモートURLの
ソースもオプトインになった。これは本物の改善であり、1年分のワームが通り抜けた
扉を一つ閉じる。
扉を一つ閉じる。攻撃者はすでにもう一つの扉を見つけていた。
ペイロードはインポート時へ移った
7月11日、npmパッケージjscramblerが侵害された
——約3時間にわたって5つの悪意あるバージョンが公開された。最初の3つ、8.14.0
から8.17.0は「preinstallフックからドロッパーを実行した」、古典的なパターンだ。
それからキャンペーンの途中で、攻撃者は扉を変えた。「8.18.0以降ではインストール
フックが完全に消え、同一のドロッパーが代わりにdist/index.jsの先頭に自己実行
関数として注入される」ため、「インストール時ではなく、パッケージがインポート
されたときやCLIが実行されたときに発火する」。Socketは理由を率直に名指しする。
「これは意図的な回避だ。preinstall/postinstallスクリプトだけを検査する
スキャナーを打ち負かし、npm install --ignore-scriptsを生き延びる」。
その3日後、npmの@asyncapi組織
が同じ手口で侵害されたが、最初からそのために作り込まれていた。4つのパッケージ
にまたがる5つのバージョン——@asyncapi/specs 6.11.2-alpha.1と6.11.2、
@asyncapi/generator 3.3.1、@asyncapi/generator-components 0.7.1、
@asyncapi/generator-helpers 1.1.1——は、攻撃者が設定を誤ったGitHub Actions
ワークフローを使ってプロジェクトのボットトークンを流出させた後に再公開された。
ここで重要なのはMicrosoftの所見だ。「影響を受けたすべてのパッケージは、
package.jsonにpreinstall、install、postinstallフックを一切宣言していなかった」。
ローダーは代わりに各パッケージの通常のエントリーポイントの中に座っていた——
specsにはindex.js、ジェネレーターのテンプレート奥深くにはvalidator.js。
そして緩和策の助言は明確だ。「npm install --ignore-scriptsを緩和策として
頼ってはならない。このキャンペーンはライフサイクルフック経由ではなく、
モジュールがインポートされたときに実行される」。
無関係な2人のアクターが、3日の間隔で、エコシステムの目玉の修正が稼働した まさにその週に、同じ手に行き着く。これは偶然ではない。防御が仕組みを名指し するとき、こうなる——その仕組みが立ち位置になり、別の場所に立つことは攻撃者 にとって数行のコードで済む。「このパッケージにインストールフックはあるか?」と 問うスキャナーは、AsyncAPIから正直な「ない」を受け取り、通してしまう。
移動しなかった唯一のもの
配送の仕組みを取り除いて、両方の侵害を通じて何が一定だったかを見よう。 jscramblerのバージョンは数時間だけ公開されていた。AsyncAPIの安定版は08:30 UTCに公開され、下流の検知は08:49に始まった——19分だ。両方のインシデントの 悪意あるバージョンはすべて、公開から2時間の十分内に取り下げられた。ペイロード はインストールフックからエントリーポイントへ移り、配送はライフサイクル スクリプトからランタイムローダーへ移った。移動しなかった唯一の性質は、毒を 仕込まれた各バージョンが真新しかったことだ。公開されたばかりのバージョンは 侵害の共通の指紋である。なぜならそれが攻撃の形だからだ——トークンを盗み、 公開し、見つかり、取り下げられる。窓は構造的に短い。
それこそがBromureの経過日数ゲートが確認する性質であり、
既定で有効だ。すべてのパッケージ取得——npm、PyPI、Cargo、その他——は、
エージェントのインストールが1バイトでも見る前にホストの中間者プロキシを通り、
プロキシはしきい値より若いどんなバージョンも拒否する。既定で2日だ。latestの
ような浮動参照やsemverの範囲は、十分に古い最新バージョンへ静かに解決される。
新鮮すぎるバージョンへの固定参照は、理由を説明するBromureのエラーとともに
451として返る。ペイロードがどこにあるかは決して問わない。コードが
preinstallで発火するかrequire()で発火するか、フックが在るか無いか、
--ignore-scriptsが設定されているかは関係ない。19分前に公開されたバージョン
は、19分前だから拒否される。この2つのキャンペーンは、2日ゲートに対して到着
時点で死んでいた——Bromureがペイロードを認識したからではなく、そもそも
ペイロードをまったく見なかったからだ。
経過日数ゲートは、新しいパッケージが悪意あるという主張ではない——ほとんどは 問題なく、2日前のリリースはまだとても新しい。それはリスクがどこに集中するか についての主張だ。侵害の危険な窓は、盗まれたトークンがバージョンを押し込んだ 後、メンテナが気づいて取り下げるまでの最初の数時間である。2日待っても パッケージが安全になるわけではないが、これらの攻撃が生きるまさにその区間から あなたのインストールを外し、しかもあなたがこれから実行するコードを一行も検査 せずにそうする。公開当日に必要なパッケージには、ゲートはパッケージ単位の例外を 認める。それ以外はすべて2日を待つ。
それでもバージョンが十分に古いとき
経過日数ゲートはフィルターであって壁ではない。より短いしきい値を設定し、初日に 必要なパッケージを除外し、あるいは2日を超えて老いるほど長く検知されずにいた 悪意あるバージョンを取り下げれば、毒入りのコードはインストールに届く。その場合 のBromureの答えは、以前のサプライチェーンの記事が説明しているものであり、経過 日数ゲートが防御のすべてではなくフィルターであることを許される理由でもある—— インストールを実行するエージェントは、あなたのMacの上にはいない。
それは使い捨てのLinux VMの中、ハイパーバイザーの境界ひとつ向こうにいる。だから
AsyncAPIのローダーが発火し、その認証情報コレクターが探しに行くとき——そして
それは激しく探し、百を超える環境変数
を掃引する。NPM_TOKEN、AWS_ACCESS_KEY、ANTHROPIC_API_KEYを含み、さらに
ディスク上の.npmrc、~/.aws/credentials、id_ed25519、kubeconfigも——
見つけるのはVMのコピーであり、それらはプレースホルダーのbrm_…文字列だ。
本物の値はホストに住む。エージェントがそれを必要とする正当なリクエストを行うと、
ホストのプロキシはその一つのホップのあいだだけ、回線上で偽物を本物の値と
入れ替え、また戻す。秘密はコレクターがかき集めるためにVMのメモリへ着地する
ことは決してない。ペイロードがコピーする~/.ssh鍵は、そのプロファイル用に
作られた使い捨てのペアだ。そしてペイロードが試みるC2アップロード——この場合は
85.137.53.71宛て——は、あらゆる宛先を記録する同じホストプロキシを通って出て
いくので、あなたが一度も起動していないNodeプロセスが未知のIPへソケットを開く
ことは、静かな転送ではなくセキュリティログの一行になる。窓が閉じると、VMと
ペイロードがその中で行ったすべては消える。
これは今年のはじめにRed Hat自身のnpmスコープ へ乗り込んだのと同じMiasmaランタイムファミリーだ。コレクターは成熟し、 キャンペーンをまたいで再利用される。変わり続けるのは配送の仕組みの側だ。
この特定の週の居心地悪い教訓は、仕組みに固定された防御は半減期が日単位だと いうことだ。エコシステムは1年をかけてインストールスクリプトを注視すべきものに し、修正をリリースし、そしてリリースノートが冷めるより前に、2つの別々の アクターがペイロードを移すのを見た。その競争は次の仕組みを名指しすることでは 勝てない。コードが発火できる場所は常に他にもあるからだ。勝つのは、攻撃者が安く 変えられないもの——バージョンが何日前か——を確認し、すり抜けたものを、何も 持ち出せない場所で実行することによってだ。試してみる。