본문 바로가기

AI Agent

Slopsquatting 완전 가이드 — AI가 추천한 패키지가 악성코드일 수 있다

반응형

Claude Code한테 "이거 구현해줘" 했더니 import fastjson 썼습니다. PyPI에서 설치하려는데 진짜 있더라고요. 근데 어제 공격자가 등록한 겁니다. 이게 Slopsquatting입니다.

[핵심 요약]
→ Slopsquatting: AI가 환각한 패키지 이름을 공격자가 선점 등록하는 공급망 공격
→ 용어 창시: Python Software Foundation 보안 연구원 Seth Larson
→ 규모: 16개 AI 코딩 모델 테스트 → 생성 코드의 약 20%에 환각 패키지 포함
→ 반복성: 환각 패키지 이름의 58% 이상이 여러 번 반복 → 공격자가 예측 가능
→ 실제 사례: react-codeshift (jscodeshift + react-codemod 합성) — 2026년 1월
→ 타겟: npm(JS), PyPI(Python), RubyGems 등 공개 패키지 레지스트리
→ 위험: CI/CD 파이프라인 자동 설치 → 탐지 전 프로덕션 배포

왜 지금 위험한가

기존 타이포스쿼팅:
→ 공격자가 패키지 이름 직접 추측
→ 개발자가 오타 낼 확률에 의존
→ 방어: 패키지 이름 주의해서 타이핑

Slopsquatting:
→ AI가 환각 패키지를 자신 있게 추천
→ 개발자는 AI 코드를 그대로 복붙
→ 패키지 이름이 그럴듯하게 보임
→ 공격자는 AI 출력 모니터링만 하면 됨
→ Vibe Coding 확산 = 공격 표면 폭발적 증가

2026년 1월 실제 사례, Aikido 보안 연구원 Charlie Eriksen이 발견했습니다.

react-codeshift 사건:
→ LLM이 jscodeshift + react-codemod 두 패키지를 합성
→ react-codeshift라는 없는 패키지 이름 생성
→ AI가 생성한 에이전트 스킬 47개 커밋에서 발견
→ 패키지 없다가 → 공격자 등록 → 실행 시 악성코드

실전 1 — 환각 패키지 탐지 스크립트

AI가 생성한 코드의 패키지를 설치 전에 검증합니다.

# check_packages.py
import subprocess
import sys
import re
import requests
from pathlib import Path

def extract_imports(code: str) -> list[str]:
    """코드에서 import 패키지명 추출"""
    patterns = [
        r'^import\s+([\w\-]+)',
        r'^from\s+([\w\-]+)\s+import',
        r'pip install ([\w\-]+)',
        r'npm install ([\w\-]+)',
    ]
    packages = set()
    for line in code.split('\n'):
        for pattern in patterns:
            match = re.match(pattern.strip(), line.strip())
            if match:
                packages.add(match.group(1))
    return list(packages)

def verify_pypi(package: str) -> dict:
    """PyPI에 패키지 존재 여부 + 메타데이터 확인"""
    try:
        res = requests.get(
            f"https://pypi.org/pypi/{package}/json",
            timeout=5
        )
        if res.status_code == 404:
            return {"exists": False, "package": package}

        data = res.json()
        info = data["info"]
        releases = data["releases"]

        return {
            "exists": True,
            "package": package,
            "version": info["version"],
            "author": info["author"],
            "release_count": len(releases),
            # ⚠️ 릴리즈가 1~2개면 최근 등록 의심
            "suspicious": len(releases) <= 2,
            "pypi_url": f"https://pypi.org/project/{package}/",
        }
    except Exception as e:
        return {"exists": False, "package": package, "error": str(e)}

def verify_npm(package: str) -> dict:
    """npm에 패키지 존재 여부 확인"""
    try:
        res = requests.get(
            f"https://registry.npmjs.org/{package}",
            timeout=5
        )
        if res.status_code == 404:
            return {"exists": False, "package": package}

        data = res.json()
        versions = list(data.get("versions", {}).keys())

        return {
            "exists": True,
            "package": package,
            "latest": data.get("dist-tags", {}).get("latest"),
            "version_count": len(versions),
            "suspicious": len(versions) <= 2,
            "npm_url": f"https://www.npmjs.com/package/{package}",
        }
    except Exception:
        return {"exists": False, "package": package}

def audit_ai_code(code: str, ecosystem: str = "pypi") -> None:
    """AI 생성 코드의 패키지 전부 검증"""
    packages = extract_imports(code)
    print(f"\n🔍 {len(packages)}개 패키지 검증 중...\n")

    dangerous = []
    suspicious = []

    for pkg in packages:
        if ecosystem == "pypi":
            result = verify_pypi(pkg)
        else:
            result = verify_npm(pkg)

        if not result["exists"]:
            dangerous.append(pkg)
            print(f"❌ {pkg} — PyPI/npm에 존재하지 않음 (환각 패키지 의심)")
        elif result.get("suspicious"):
            suspicious.append(pkg)
            print(f"⚠️  {pkg} — 릴리즈 수 적음, 최근 등록 가능성")
        else:
            print(f"✅ {pkg} — 정상")

    print(f"\n{'='*40}")
    print(f"❌ 위험 패키지: {dangerous}")
    print(f"⚠️  의심 패키지: {suspicious}")

    if dangerous or suspicious:
        print("\n⛔ 설치 전 수동 검증 필요!")
        sys.exit(1)

# 사용 예시
ai_generated_code = """
import fastjson
from aws-helper-sdk import S3Client
import react-codeshift
"""

audit_ai_code(ai_generated_code, ecosystem="pypi")
[탐지 기준]
→ 존재하지 않는 패키지: 즉시 차단
→ 릴리즈 1~2개: 최근 등록 의심 → 수동 검토
→ 작성자 정보 없음: 의심
→ GitHub 링크 없음: 의심
→ 다운로드 수 0: 신규 등록 가능성 높음

실전 2 — CI/CD 파이프라인 방어

# .github/workflows/security-check.yml
name: Slopsquatting 방어

on:
  pull_request:
    paths:
      - 'requirements*.txt'
      - 'package*.json'
      - '*.py'
      - '*.ts'
      - '*.js'

jobs:
  package-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Python 패키지 검증
        run: |
          pip install requests
          python check_packages.py requirements.txt

      - name: npm 패키지 검증
        run: |
          # 존재하지 않는 패키지 설치 시도 차단
          npm install --dry-run 2>&1 | grep -i "404\|not found" && exit 1 || true

      - name: pip-audit 실행
        run: |
          pip install pip-audit
          pip-audit -r requirements.txt

      - name: Socket 보안 스캔 (npm)
        uses: nicolo-ribaudo/npm-package-socket-security-check@v1
        # Socket은 환각 패키지 전용 탐지 DB 보유
# pre-commit hook — 커밋 전 자동 검사
# .git/hooks/pre-commit

#!/usr/bin/env python3
import subprocess
import sys

def check_new_imports():
    """스테이징된 파일에서 새로 추가된 import 검사"""
    result = subprocess.run(
        ['git', 'diff', '--cached', '--name-only'],
        capture_output=True, text=True
    )
    changed_files = result.stdout.strip().split('\n')

    for file in changed_files:
        if file.endswith('.py'):
            diff = subprocess.run(
                ['git', 'diff', '--cached', file],
                capture_output=True, text=True
            ).stdout

            # 새로 추가된 import 줄만 추출
            new_imports = [
                line[1:] for line in diff.split('\n')
                if line.startswith('+import ') or line.startswith('+from ')
            ]

            if new_imports:
                print(f"⚠️  {file}에 새 import 발견:")
                for imp in new_imports:
                    print(f"   {imp}")
                print("PyPI/npm 존재 여부를 확인하세요.")

check_new_imports()
[CI/CD 방어 레이어]
→ PR 단계: requirements.txt, package.json 변경 시 자동 검증
→ 커밋 단계: pre-commit hook으로 새 import 경고
→ 빌드 단계: pip-audit, npm audit 실행
→ 런타임 단계: Socket, Snyk으로 실시간 모니터링
→ 핵심: 자동 설치 파이프라인에 검증 게이트 추가

실전 3 — Claude Code / Cursor 사용 시 방어 설정

AI 코딩 툴 자체에서 환각 패키지를 줄이는 설정입니다.

# CLAUDE.md — 패키지 환각 방지 규칙

## 패키지 사용 규칙

새 패키지를 추천하기 전에 반드시:
1. 해당 패키지가 실제 존재하는지 확인
2. PyPI(https://pypi.org) 또는 npm(https://npmjs.com)에서 검색
3. 공식 문서 링크 제공
4. 마지막 업데이트 날짜 확인

절대 하지 말 것:
- 확인하지 않은 패키지 이름 사용
- 두 패키지를 합성한 이름 생성 (예: requests + httpx → requestx)
- 패키지 존재 여부 확실하지 않으면 표준 라이브러리 사용

패키지 추천 시 반드시 포함:
- PyPI/npm URL
- GitHub 저장소 링크
- 주간 다운로드 수 (인기도 확인용)
# pip 설치 전 확인 스크립트
# 팀 공통 alias로 등록

# ~/.bashrc 또는 ~/.zshrc
alias safe-pip='python -c "
import sys, requests
pkg = sys.argv[1]
r = requests.get(f\"https://pypi.org/pypi/{pkg}/json\")
if r.status_code == 404:
    print(f\"❌ {pkg} — PyPI에 없는 패키지!\")
    sys.exit(1)
data = r.json()
releases = len(data[\"releases\"])
print(f\"✅ {pkg} v{data[\"info\"][\"version\"]} — {releases}개 릴리즈\")
if releases <= 2:
    print(\"⚠️  릴리즈 수 적음 — 수동 검토 권장\")
    input(\"계속하려면 Enter...\")
" "$1" && pip install "$1"'

# 사용: safe-pip requests
[AI 코딩 툴별 환각 위험도]
→ Claude Code (Sonnet 4.x): 낮음 — 최신 모델, 환각률 상대적으로 낮음
→ GitHub Copilot: 중간
→ 오픈소스 모델 (CodeLlama 등): 높음 — 연구에서 환각률 가장 높음
→ 공통: 어떤 모델도 100% 안전하지 않음 → 항상 검증 필요

마무리

✅ 반드시 해야 하는 것
→ AI 생성 코드의 패키지 PyPI/npm에서 수동 검색
→ CI/CD에 패키지 검증 게이트 추가
→ CLAUDE.md/CURSOR.md에 패키지 확인 규칙 명시
→ pip install 전 릴리즈 수·다운로드 수 확인
→ Socket, Snyk 같은 공급망 보안 도구 도입

❌ 절대 하면 안 되는 것
→ AI 추천 패키지 아무 확인 없이 바로 설치
→ CI/CD에서 패키지 자동 설치 후 검증 없이 배포
→ 릴리즈 1~2개짜리 신규 패키지 무조건 신뢰
→ "유명한 패키지 이름 같으니까 괜찮겠지" 판단
→ Vibe Coding 결과물을 검토 없이 프로덕션 배포

관련 글

 

https://cell-devlog.tistory.com/161

 

5개국 "에이전트 AI 보안 가이드" 완전 분석 — 정부가 경고한 AI 에이전트 5가지 위험과 개발자 체

CISA, NSA, 영국, 호주, 캐나다, 뉴질랜드가 함께 경고했습니다. AI 에이전트는 이미 핵심 인프라에서 돌아가고 있고, 대부분의 조직이 아무도 실시간으로 감사할 수 없을 만큼 많은 권한을 줬다고.[

cell-devlog.tistory.com

https://cell-devlog.tistory.com/98

 

AI 코딩 툴 보안 실전 — Claude Code 소스 유출 이후 달라진 공격 지형

2026년 3월 31일, Claude Code 소스코드가 npm에 실수로 노출됐어요.유출 규모:→ 51만 2천 줄 TypeScript→ 1,906개 파일→ 24시간 만에 GitHub 포크 41,500개공격자들의 반응 속도:→ 유출 24시간 이내: 악성 "leak

cell-devlog.tistory.com

https://cell-devlog.tistory.com/27

 

AI 에이전트 보안 완전 정리 — Prompt Injection 공격과 방어 완전 가이드

AI 에이전트를 프로덕션에 올리면 이런 일이 생겨요."에이전트가 갑자기 이상한 행동을 해. 아무도 그런 지시를 안 했는데."이건 버그가 아니에요. 공격이에요. OWASP 2025 LLM Top 10에서 Prompt Injection

cell-devlog.tistory.com

 

반응형