created_at
updated_at
tags
toc

サプライチェーン攻撃への防御策

Intro

前回は、Nx の事例をベースに「パッケージを公開する側」の対策について解説した。

今回は、「パッケージを使う側」、もっと言えば「OSS を使う上で開発者が考えるべきこと」について考察する。

OSS の危険性

npm 起因のサプライチェーン攻撃が確認されたことで「npm は危険だ」という話になると、「npm を禁止すべき」といった極端な話になったりする。

前回のブログで紹介したような対策を行うなら、多少は良くなるかもしれない。しかし、それらは全てパッケージ公開者に委ねられる。自分が公開者として実施するなら、自分が原因で攻撃が発生することは防げるだろう。

一方、攻撃に必要な突破口は 1 つあれば良い。npm にある全てのパッケージが対策されない限り、npm を主語とした安全が担保される日は来ない。

この広大な依存関係の中には、闇落ちした開発者が、それまでの善良なコードを、自分の意志で攻撃コードに置き換えて公開することもある。

そう、OSS とは、本質的にそういうものだ。

誰かが公開したコードを、ライセンスに基づいて活用しているだけ。そこに安全の保証などない。

我々は、自分たちで全てのコードを書き、サービスを運用することなどできない。代わりに共通資産としてのコードを育て、危険性と天秤にかけた経済合理性を享受している。

つまり npm は「危険になった」のではなく「最初からずっと危険だった」のだ。

もっと言えば、OSS とは誕生したときからずっとそうだった。

我々は、全てのコードをチェックすることも叶わなくなり、祈るようにコードをチェックアウトしてきた。

いつしか祈ることすら忘れ、「潜在的に危険なコードを受容する体制づくり」を怠っていたことが、今回の件で表面化しただけと捉えるべきだろう。

npm 以外の対策

npm を中心とした議論では、socket.dev などを使ってフィルタリングを行うといったソリューションや、pnpm に移行して minimumReleaseAge を指定するといったプラクティスもある。

Nx の攻撃はほぼ数時間の間に発生したものだ。仮に 4h として、セキュリティベンダが攻撃発生から 3h で発見できたとしても、1h は攻撃が成立している状態だ。そこで、公開から 1~2 日猶予を持てば被害は軽減できるとし、アップデートを遅らせるのが minimumReleaseAge だ。しかし、それは同時に緊急のセキュリティフィックスが適用されるまでの、ゼロデイ可能性にも寄与する両刃の剣となる。

また、npm の対策がある程度盤石となったとして、同じ手法を gem や pip や brew その他あらゆるパッケージマネージャーに応用しない根拠はない。

これは完全に妄想だが、今回の攻撃は、おそらく誰かが既知の攻撃をパッケージ化し、ダークウェブ上で商品として販売し、誰かがそれを買って実行したのだろうと思う。そうであれば、別の攻撃が起こるかどうかは、そのパッケージの別バージョンを作って売る人と、それを買って実行する人が現れるかどうかの差でしかない。

最近では、MCP のレジストリなども出てきており、我々が運用に慣れるまでの黎明期はおそらく狙い放題、格好の餌食になる可能性もある。

npm 一箇所を対策したことは、それ以外の対策を怠っていることの裏返しでしかないのだ。

「やっぱり OSS は危険だ、使うべきではない」と決別するのは自由だが、普通に考えれば、それをするには我々は OSS の良さも十分に知ってしまった。

捨てたところで、今以上に良くなる保証は、もはやできない。

では、我々は何ができるだろうか?

危険なコードができること

危険なコードを受容するには、この広義の「危険なコード」で何ができるかを考える必要がある。

任意の処理が実行できるマルウェアでも動かせれば、できることは無限にある。しかし、「攻撃者にとってメリットのある攻撃」は、実はある程度パターンが絞られるだろう。

大抵「壊す」か「盗む」かだ。

壊す場合は、OS やハードまで含め、機能不全に陥れる状態だ。重要なプロセスを掌握してシステムを停止したり、まるっと暗号化して身代金を要求するような攻撃がそれにあたる。

盗む場合は、クレデンシャルと称される NPM_TOKENGITHUB_TOKEN、SSH の鍵、パスワード、個人情報、仮想通貨の鍵など、それ自体に価値のあるものか、次の攻撃に転用できるものを狙う。

取りうる対策

そんなことが起こる可能性があるコードを、我々は日々取得して利用しているのだ。リスクは最近生じたわけではない。

その中で、「感染しない」ことを求めることが難しいのであれば、「感染しても大丈夫」な状態に近づけるために、レジリエンシーを上げていく方向を考えるのが妥当だろう。

過去にも類似する攻撃は何度も発生し、その上で様々な対策や緩和策が考えられてきた。

その中で、筆者が見直したいと思うのは、大きく分けて 2 つだ。

クレデンシャルの秘匿

パスワードを ~/Desktop/パスワード.txt に書いて置いておくのは、誰が見ても駄目だとわかるだろう。

ところが、Personal Access Token を ~/develop/my-project/.env に書いて置いておくのは、なぜか一般的に行われている。

運用が面倒だからと広い権限が付与され、更新が面倒だからと no expire な期限が付与されることも、少なくないだろう。

昨今の攻撃には様々な要因があるが、「盗まれたもの」は基本的にはこうした *_TOKEN の類だ。

また、こうしたクレデンシャルは、環境変数を経由してプロセスに渡されることが多い。

環境変数も、グローバルにアクセスできる平文のファイルのようなものだ。そのうえで、以下のような設定が環境構築で指示されるようなことも多い。

# 環境変数を設定
echo GITHUB_TOKEN="your-access-token" >> .zshrc

これは、この zsh で実行する全てのプロセスでアクセス可能であることを意味する。本来、開発中のサーバでしか使わない値であっても、同じ zsh で起動した他のプロセスからアクセスができるため、Infostealer からすれば盗み放題だ。

この状況を改善するためには、まず「クレデンシャルを平文でファイルに保存するのをやめる」が最初にあるだろう。

パスワードと同様に考えて、.zshrc.env にあるクレデンシャルを全てパスワードマネージャに保存し、例えば Touch ID を経ないと開けないように暗号化して管理してしかるべきだ。

ところが、その値を必要とするプロセスには平文で渡す必要がある。

1Password の場合は op コマンドを使えば、起動しているプロセスにのみ、1Password 内のクレデンシャルを渡すことも可能だ。(ファイルを用いた書き方など、方法は複数ある)

$ NPM_TOKEN="op://develop/npm/token" op run -- npm start

クレデンシャルそのものがファイルにはなく、Touch ID を伴わないと出てこない。これにより、こっそり動いている infostealer はクレデンシャルを盗む方法がなくなるのだ。

もしクレデンシャルをファイルに平文で書いてるのであれば、格段に安全になるだろう。op 以外にも同様のことができるツールはある。

ただし、この npm start のプロセス自体が compromise されていれば、もちろん環境変数は盗める。そこを安全にするには、環境変数を Bearer Token にするのをやめるしかない。つまり、この世界にも Passkey のような考え方が必要になる。パスワードと同じ道を開発用トークンもたどるというのは、自然な発想だと筆者は考えているため、いずれはそうなるかもしれない。

コンテナでの開発

自分の PC でターミナルを開き、そこで npm i をするような行為は、感染した結果生じる被害がその PC 全体に及ぶ。

.env~/.ssh/* をパスワードマネージャに隠したとしても、~/Downloads~/Desktop は、日常的に情報が蓄積されやすい、秘情報の宝庫だ。最近の Mac はここへのアプリアクセスに権限がついたが、安易に許可している人も多いだろう。

昨今の攻撃では、ここらへんは回収対象になっていない。おそらく、もっと簡単に換金できる情報(仮想通貨ウォレットなど)を集める方が効率が良いからだ。盗めないのではなく、たまたま盗んでいないだけだ。攻撃者のモチベーションやインセンティブが変われば、矛先を変えるのは造作もない。その PC が社内ネットワークにアクセスできることを利用し、踏み台として攻撃を広げていくことも可能だ。

つまり、「普段使っている業務用 PC で、そのまま開発作業をする」という事自体が、危険な行為だったと言える。それも、別に今始まったことではない、ずっと危険だったのだ。

ここに対して、最も端的に効果が出やすいのがコンテナでの開発だ。VirtualBox、Docker、Dev Container など、選択肢はいくらでもある。

これらを入れ、業務用と開発用の環境を分離(Isolation)することで、仮に攻撃を受けても開発用環境しか影響を受けないように封じ込めることができる。

開発用環境の中でさらにクレデンシャルが隠せていれば、盗めるものはかなり限られてくる。標的型で「ソースコードを盗む」自体が目的にでもなっていれば別だが、ばらまき型のサプライチェーン攻撃を受けて発生する被害は、最小限に抑えられると考えられるだろう。

もちろん、コマンドのインストールや実行自体を細かく Docker でくるんだり、Deno のように権限を指定できるサンドボックス環境で実行したりと、他の選択肢もある。

しかし、「一個一個気をつける」という運用は、認知の負荷が上がっていくだけで、人間には向かない。

まるっと Dev Container を用意し、最初に細かな設定に気を使ったら、あとは「開発作業はこの中でやる」「余計なファイルを持ち込んだり、ディレクトリをマウントしない」といったルールの方が運用負荷も低いだろう。特に VSCode を使っているなら、立ち上げるワークスペースが変わるだけで、かなり透過的に実現できるはずだ。

Mac を使う多くの開発者は、brew ベースで開発環境を整えるような運用をしていると思う。筆者もそうだ。

これ自体がリスクのある行為だったことを認め、あらゆる開発を Dev Container に移行するように筆者自身もやっていきたいと思っている。

Outro

今回の件を受けて、「npm は危険だから使用禁止」や「brew は入れるモジュールを都度申請」などと、一見リスクを回避していそうで何の解決にもなっていないルールが課され、仕事を邪魔されている現場もあるかもしれない。

OSS は確かに危険だ。しかし、だからといってエコシステムを全て捨てられるほど、我々の開発は独立してない。むしろ、コードが衆目に晒され、数多くのコントリビューターによって修正されたバグや、脆弱性があることを考えると、node_modules 以下を全部自前で書き直したところで、OSS 以上に安全になることもないだろう。

何も対策せずに npm や OSS を批判するのもナンセンスだし、benefit と risk、solve と mitigation をきちんと見極めずに OSS と決別するのはもっとナンセンスだ。するべき判断と対策をサボっているだけでしかない。

今まで我々が甘んじてきた牧歌的な開発環境を見直し、OSS のリスクとベネフィットを正しく受け入れる体制を、見直すべき時が来たのだと考えたい。