本記事は、GlassWormの仕組みを自分なりに整理しながら理解することを目的にまとめたものです。誤りなどございましたらコメントにてご指摘いただけますと助かります。
はじめに
先日、業務で使用しているaxiosに対するサプライチェーン攻撃に伴う脆弱性対応を行いました。
今まで特に脆弱性対応などは行ったことがなく、非日常感がありワクワクしたのと同時に、「サプライチェーン攻撃ってこんなにも身近なものなのか」と衝撃を受けました。
それをきっかけに、最近のサプライチェーン攻撃について調べたところ、個人的に非常に衝撃を受けた攻撃手法を見つけました。
それが GlassWorm です。
2025年の10月にKoi Securityによって報告された攻撃です。
これは Unicodeの不可視文字を悪用して、コードエディタや差分表示では気づきにくい形で、不可視文字列としてペイロードを埋め込むというものです。
2026年3月の報告では、GlassWorm は 150件超の GitHub リポジトリや 72件超の OpenVSX 拡張機能に関与していたとされています。出典
今回は、このGlassWormがどういったものなのかを、自分なりにまとめていきます。
そもそも文字はどのようにコンピュータ上で扱われているのか
GlassWormの手口を理解するためには、まずコンピュータがどうやって文字を扱っているのかを理解する必要があります。
コンピュータが扱える情報は数字のみです。文字を直接「こういう形の文字」として扱うことはできません。ではどうするのかというと、それぞれの文字に番号(コードポイント)を割り振り、その番号から対応する文字を表現します。
この「数字と文字の対応」の規格として扱われるのが Unicodeです。
このUnicodeには非常に多くの文字が定義されています。
たとえば、
- 「あ」 →
U+3042(10進数で12354)
コードポイントは16進数で表記されます。
U+というのは、「これはUnicodeのコードポイントですよ」という表記上のラベルで、実際のデータとしてはただの数字です。
Variation Selector
話は変わって、日本語には同じ文字ではあるけれども、形が微妙に異なる漢字が存在します。
たとえば 「辻」 と 「辻󠄀」
しんにょうの点が1つか2つの違いですが、これらはどちらも存在します。
しかし表す漢字の意味的情報はどちらも同じです。
多くの場合はどちらでも問題ありませんが、文書や人名表記によっては字体を区別したい場面もあります。
しかし先述したUnicode規格では、これらの文字にはどちらも同じ U+8FBBが基本文字として割り当てられています。
つまり、基本のコードポイントだけでは字体の違いを表現できません。
ここで出てくるのが Variation Selectorです。
U+8FBBの後ろに、Variation Selectorを付与することによって、「先のコードに対応する文字を、この字体で表示してください」と指定することができます。
つまり
-
U+8FBBの後ろに、U+E0100というVariation Selectorを付与すると 「辻󠄀」 -
U+8FBBの後ろに、何も付与しないと 「辻」
となります。
Variation Selectorは「見えない付箋」のようなものであり、 それ自体は画面に表示されません。
また、対応する文字コードが直前にない場合も、多くの環境でエディタは「無効な組み合わせ」としてこのコードを無視し、何も表示しません。
つまり、Variation Selectorを単体でファイルに大量に配置しても、多くの環境においてエディタ上では見えないということです。
GlassWormはこれをどう悪用したのか
GlassWormの攻撃者は、「画面には表示されないが、データとしては存在する」というVariation Selectorの性質に目をつけました。
Variation Selectorは以下の2つの範囲に存在します。
| 範囲 | 個数 | 変換後の値 |
|---|---|---|
U+FE00 〜 U+FE0F
|
16個 | 0 〜 15 |
U+E0100 〜 U+E01EF
|
240個 | 16 〜 255 |
これらは合計すると 256種類であり、0から255の全バイト値を表現できます。
つまり、 あらゆるコードを不可視文字の列に変換できるということになります。
たとえば、文字 cを不可視文字列にする場合、 cのUTF-8バイト値は16進数で63(10進数で99)です。
これをバイト値99としてVariation Selectorに変換すると、
U+E0100 + (99-16) = U+E0153という1つの不可視文字で表現できる
しかしこれだけだと、ただの不可視文字でしかないので、実際にはこれをUnicodeの形にデコードしてやる必要があります。
実際に使用されたデコーダーコード
以下は、GlassWormで実際に使用されたデコーダー(出典:https://www.koi.ai/blog/glassworm-hits-mcp-5th-wave-with-new-delivery-techniques)
const s = v => [...v].map(w => (
w = w.codePointAt(0),
w >= 0xFE00 && w <= 0xFE0F ? w - 0xFE00 :
w >= 0xE0100 && w <= 0xE01EF ? w - 0xE0100 + 16 :
null
)).filter(n => n !== null);
eval(Buffer.from(s(``)).toString('utf-8'));
-
``(バッククォート)の中身 : 見た目は完全に空ですが、実際には大量のVariation Selectorが詰まっています。 -
w.codePointAt(): 不可視文字のコードポイント番号を取得します -
w - 0xFE00/w - 0xE0100 + 16:コードポイントからバイト値(0〜255)を計算します -
Buffer.from(...).toString('utf-8'):バイト値の配列を文字列に復元します -
eval(...):復元した文字列をJavaScriptコードとして実行します
たったこれだけのコードで攻撃が成立してしまいます
なぜ見つけにくいのか
- 悪意のあるコード自体が不可視
エディタで見ても、GitHubのdiffで見ても、悪意のあるコード自体は一切表示されません。「空の文字列」が見えるだけです。
- デコーダーは無害に見える
実際の攻撃では、変数名がprocessDataやbufferのように一見それらしいものになっている。数百ファイルあるパッケージの中のたった数行でしかないので、非常に気づきにくい。
対策
-
不審なMCPサーバーや依存関係の排除
不必要なMCPサーバーの使用を控え、外部の依存関係を可能な限り減らすことが推奨されています。 -
拡張機能の精査
VS CodeやOpenVSXの拡張機能など、インストール済みのものを定期的に確認し、必要最小限のもの以外は削除するようにしましょう。 -
権限とトークンの管理
GitHubトークンなどのスコープを必要最低限に制限しましょう。
