ひむ日記

本名は設楽です

サプライチェーン攻撃と正常性バイアス

現代において、個人であれ企業であれソフトウェアを開発するにあたって少なからず誰しもがサードパーティのツールなりサービスに依存している。
そのような依存関係を悪用して標的のネットワークやシステムに侵入する攻撃である「(ソフトウェア) サプライチェーン攻撃」という手法が存在する。

最近では Nx リポジトリに対する攻撃が記憶に新しい。
Malicious versions of Nx and some supporting plugins were published · Advisory · nrwl/nx · GitHub

今後いかにセキュリティ技術が進歩したところで、オープンソースのエコシステムを人間が運営する以上はこのようなミスに起因するアクシデントを完全に防ぐことは不可能であり、各ユーザーそれぞれが責任を持って対策することが重要である。





という教科書のような講釈はそこかしこで叫ばれており、インターネットの海を漂っていると一度は見たことがある話なのではないだろうか。

しかしこれらの情報を以って実際に、業務で用いるコードのセキュリティを見直したり個人で使っている依存関係のセキュリティチェックを実施したりといった行動を起こしたユーザーはあまり多くはないと思う。

実情としては「なんやかんや世界中の優秀なエンジニアの方々のおかげで早期に発見されて既にパッチが当てられているし、身近にサプライチェーン攻撃を受けて大変なことになったユーザーがいるわけでもないし、そもそも忙しくてセキュリティを見直している暇ないし、、、」というユーザーがほとんどな気がする。

かくいう僕も上記のような認識しかしておらず、Nx に対する攻撃のメカニズムを解説した記事を読んだ上で「GitHub Actions くん、入力された文字列をコードとして解釈し得るんだ。ShellCheck みたいなツール作ったら需要あるかもな」くらいの感想を抱いただけで特に何も行動していなかった。

事件

つい昨日 chalk や debug をはじめとする npm パッケージが侵害されたことが判明した。
npm debug and chalk packages compromised

これも Nx 同様他人事として流しそうになったのだが、昔 pnpm のファイル構造を趣味で調べていたときに chalk, debug というパッケージ名を見た覚えがあり、他人事ではないかもしれん、ということに気づいた。

というのも、npm は依存しているパッケージが依存しているパッケージも再帰的にインストールするので、package.json に書かれていなくともしれっとインストールされている、というようなパッケージが存在する。

実際、以下のコマンドを用いて自分が管理するプロジェクトの環境を見てみると、next, eslint, vitest を通じて chalk と debug に依存していることが分かった。

$ grep -r "chalk\|debug" package.json node_modules/*/package.json
node_modules/eslint/package.json:    "chalk": "^4.0.0",
node_modules/eslint/package.json:    "debug": "^4.3.2",
node_modules/next/package.json:    "@types/debug": "4.1.5",
node_modules/next/package.json:    "debug": "4.1.1",
node_modules/vitest/package.json:    "@types/debug": "^4.1.12",
node_modules/vitest/package.json:    "@types/debug": {
node_modules/vitest/package.json:    "debug": "^4.4.1",
node_modules/vitest/package.json:    "@types/debug": "^4.1.12",

以下のコマンドを用いて調べたところ、幸い今回に限ってはマルウェアが仕込まれたバージョンのパッケージはインストールされていなかった。

rg -uu --max-columns=80 --glob '*.js' _0x112fa8

Ref: https://github.com/chalk/chalk/issues/656#issuecomment-3266880534
が、単に運が良かっただけでタイミングが悪ければ手元の環境にマルウェアが仕込まれていただろう。
今回のケースでは fetch が呼ばれることで悪意のあるコードが実行されるようになっていたが、もちろん手元のコードにも fetch を呼んでいる箇所は大量にあるので、もしマルウェアが仕込まれていたら悪意のあるコードも元気よく実行されていたであろうことを考えるとヒヤッとする。

対策

今回僕が被害を免れた要因の一つとして、対象となるパッケージに直接的に依存していなかったということが挙げられる。
僕は直接的に依存しているパッケージを Dependabot を用いてアップデートしており、さらにはパッチバージョンの更新に関しては自動的にマージされるようにしているため、今回マルウェアを仕込まれたパッケージに直接的に依存していた場合は、レビューなしでマージされていた可能性が高い。

今回は運良く直接的に依存しているパッケージが攻撃されなかっただけで、今後直接的に依存しているパッケージが攻撃されるということは起こり得る。
そうなった場合に、現在の状態では何の障壁もなく簡単に手元の環境に侵入されてしまう状況であることが分かった。

そのため、取り急ぎ Dependabot の cooldown という機能を用いて、リリース後一定期間経った更新のみを生成するようにした。*1
https://docs.github.com/ja/code-security/dependabot/working-with-dependabot/dependabot-options-reference#cooldown-

これにより一定期間プロダクションで活躍した"信頼のある"バージョンのパッケージしか取り込まれなくなり、ある程度のセキュリティの向上が見込める。*2

また、永遠に言われ続けている話ではあるが、ローカルの ~/.ssh に含まれるクレデンシャルを 1Password などを用いて隠すだったり、シェルの履歴に含まれるクレデンシャルを https://github.com/secretlint/secretlint を用いてマスクするなど、ローカル環境にクレデンシャルを残さないようにすることで万が一自身の環境で悪意のあるコードが実行されたとしても被害が最小限になるように準備しておくことも重要だろう。

総括

最近筆者の周りでは Covid-19 なり夏風邪なりが流行っており、改めて手洗いうがいに気をつけようと気を引き締めている今日この頃だが、ソフトウェアのセキュリティについても同様に身近なものとして適切に認識し、予防する姿勢を持つことが大切だと感じた。

手洗い・うがい・予防的統制を標語に、管理するソフトウェア共々健康に過ごしていきたい。

*1:Renovate なら minimumReleaseAge

*2:全人類がこれを導入した結果脆弱性の発見が遅れる、みたいな未来があったらそれはそれで面白い