
AI가 제안한 pip install, 그 이름은 정말 존재하나요? — 패키지 환각과 slopsquatting으로부터 의존성을 보호하기 위한
요약
AI가 생성한 코드 내 존재하지 않는 라이브러리 이름을 공격자가 선점하여 악성 패키지를 등록하는 'slopsquatting' 공격의 위험성을 경고합니다. AI의 환각(Hallucination) 현상을 악용한 새로운 보안 위협의 구조와 대응 필요성을 다룹니다.
핵심 포인트
- slopsquatting은 AI의 환각을 이용해 존재하지 않는 패키지 이름을 선점하는 공격임
- 기존 typosquatting이 인간의 오타를 노렸다면, slopsquatting은 AI의 오류를 노림
- AI가 제안한 라이브러리 설치 명령어를 검증 없이 실행하는 것은 매우 위험함
- 패키지 레지스트리(PyPI, npm 등)에 등록된 실제 존재 여부를 반드시 확인해야 함
솔직히 말씀드리겠습니다.
AI에게 코드를 작성해 달라고 했을 때, 그 안에
pip install super-fast-json
이라든가
npm install react-use-toast-pro
와 같은 행이 있을 때, 이름을 의심하지 않고 그대로 복사해서 실행한 적 없으신가요?
저는 있습니다. 왜냐하면 AI는 아주 자신만만하게 말하거든요. "이 라이브러리를 사용하면 한 번에 해결됩니다"라는 듯한 얼굴로 말이죠.
하지만 잠시 생각해 봅시다. 그 패키지, 정말 이 세상에 존재하나요?
사실, AI 코딩이 당연해진 지금, 가장 조용하게, 가장 깊은 곳을 찌르려 하는 공격이 있습니다. 그 이름을 **slopsquatting (슬롭스쿼팅)**이라고 합니다. 대략적으로 말하자면, "AI가 자주 입에 올리는, 존재하지 않는 라이브러리 이름"을 공격자가 선점하여 실제 레지스트리(Registry)에 등록해 두는 수법입니다.
당신이 AI가 말한 대로 복사해서 붙여넣는 순간, 그 "존재하지 않아야 할 이름"이 어느샌가 "공격자가 준비한 진짜 패키지"로 바뀌어 있는……. SF 영화 같지만, 이것은 2026년 현재 실제로 일어나고 있는 이야기입니다.
이 기사는 겁을 주기 위한 글이 아닙니다. AI에게 안심하고 개발을 맡기기 위한 "울타리"를 오늘부터 직접 만들기 위한 실전 가이드입니다. 코드와 프롬프트를 그대로 가져가실 수 있도록 내용을 알차게 구성하겠습니다.
무지함 때문에 뒤처지는 일이 없도록, 우선 용어부터 정리해 봅시다. 모르는 용어 때문에 흐름을 놓치는 것이 가장 아까우니까요.
패키지 / 라이브러리 (Package / Library)… 다른 사람이 만들어 준 편리한 부품. 스스로 처음부터 작성하지 않고 import하여 사용할 수 있습니다.
레지스트리 (Registry)… 그 부품들이 나열되어 있는 공식 선반. Python이라면 PyPI, JavaScript/Node.js라면 npm이 유명합니다. pip install이나 npm install은 이 선반에서 부품을 가져오는 동작입니다.
할루시네이션 (Hallucination, 환각)… AI가 사실이 아닌 것을 마치 사실인 것처럼 자신만만하게 말해버리는 현상. 여기서는 "실재하지 않는 라이브러리 이름을 실재하는 것처럼 권하는 것"을 가리킵니다.
typosquatting (타이포스쿼팅)… 예전부터 있던 수법으로, requests에 대해 reqeusts와 같이 "오타를 노린 가짜"를 레지스트리에 올려두는 것입니다. 인간의 타이핑 실수를 낚는 함정입니다.
그리고 본론인 slopsquatting은 이 typosquatting의 진화형입니다. 무엇이 다르냐 하면, 노리는 대상이 인간의 실수가 아니라 AI의 환각이라는 점입니다.
비유하자면 다음과 같습니다.
길을 잃은 당신이 아주 박학다식한 안내역(AI)에게 "근처에 맛있는 카레집 있어?"라고 물었더니, "있어요, "슈퍼 카레 골목"입니다"라고 즉답을 받았습니다. 하지만 사실 그런 가게는 존재하지 않았습니다. 그런데 그 안내역이 매번 똑같이 "슈퍼 카레 골목"이라고 말한다는 사실을 알아챈 누군가가, 선점하여 그 이름으로 가짜 가게를 냈다면……. 당신은 안내대로 들어갔다가 속수무책으로 당하게 됩니다.
이것이 slopsquatting의 구조입니다. AI가 "매번 똑같은 거짓 이름"을 말한다는 예측 가능성이 그대로 공격의 입구가 되어버리는 것입니다. 이것이 핵심입니다.
"하지만 그런 일은 별로 안 일어나지 않을까?"라고 생각하시겠죠. 저도 처음에는 그렇게 생각했습니다.
그런데, 2025년 보안 국제 컨퍼런스 USENIX Security에서 발표된 대규모 연구 ("We Have a Package for You!", arXiv:2406.10279)를 읽어보면 꽤 서늘해집니다. 이 연구는 상용 및 오픈 소스를 합쳐 16개의 코드 생성 모델에게 57만 6천 건의 코드를 작성하게 한 뒤, 권장된 패키지가 정말로 실재하는지를 하나씩 대조한 것입니다.
주요 결과를 사실로서 나열하겠습니다.
- 권장된 패키지 중,
약 19.7%가 애초에 레지스트리에 존재하지 않았음 - 상용 모델의 평균은
약 5.2%, 오픈 소스 모델의 평균은 약 21.7% (일부는 33% 초과) - 가장 우수한 모델조차도
**약 3.59%**는 환각을 일으킴 - 존재하지 않는 고유한 이름은,
20만 5천 개 이상 발견됨 - 게다가, 같은 질문을 10번 반복하면,
약 43%가 매번 완전히 동일한 환각 이름을 내놓음
마지막 것이, 은근히 가장 무섭지 않나요? 저는 이 부분이 본질이라고 생각합니다. AI는, 터무니없는 말을, 재현성을 가지고 말한다는 점입니다. 만약 무작위로 틀려준다면 공격자도 목표를 정하기 어렵겠지만, 매번 같은 이름을 말한다면 공격자는 "AI가 자주 말하는 거짓 이름 Top 100"을 만들어 미리 등록하고 기다리기만 하면 된다는 뜻이 됩니다.
실제로 환각(Hallucination) 이름이 그대로 실질적인 피해로 이어진 사례도 있습니다. 어떤 환각 패키지 이름은 3개월 동안 3만 회 이상 다운로드되었다는 보고가 있습니다. 누군가 악의를 품고 그 안에 내용을 채워 넣었다고 생각하면, 이것은 이제 "드문 사고"가 아니라, 구조적인 현상으로서 마주해야 할 주제라고 생각합니다.
만약을 위해 보충하자면, 이것은 "AI가 나쁘다"라거나 "특정 모델이 문제다"라는 이야기가 아닙니다. 우수한 상용 모델에서도 몇 %는 발생합니다. 즉, 누구의 환경에서도 일어날 수 있는 일입니다. 그렇기에 모델을 탓하는 것이 아니라, 시스템(Mechanism)으로 받아들여야 한다는 이야기입니다.
이 부분을 이해해 두면 방어의 핵심을 알 수 있습니다.
LLM (AI)는 거칠게 말하자면 "다음에 올 것 같은 문자열을, 확률적으로 그럴듯하게 생성하는" 기계입니다. pip install 의 뒤에 올 내용으로, 학습 데이터에 있었던 무수한 패키지 이름의 "파편"들을 조합하여, **"정말 존재할 법한 이름"**을 만듭니다.
문제는 AI가 "그 이름이 정말 레지스트리(Registry)에 있는지"를 확인하지 않는다는 것입니다. 존재 여부를 체크하는 기능이 내장되어 있는 것이 아니라, 어디까지나 "있을 법한 것"을 내놓을 뿐입니다. 그런데 말투만은 자신만만합니다. 이 "자신만만한 얼굴"과 "확인되지 않은 내용" 사이의 격차야말로 함정의 정체입니다.
환각이 나타나는 방식에는 크게 3가지 패턴이 있습니다. 연구에서 분류된 것을 더미(Dummy) 이름으로 정리해 두겠습니다.
| 패턴 | 발생하는 현상 | 예시 (모두 더미) | 알아채기 어려운 이유 |
|---|---|---|---|
| 혼동 (Conflation · 약 38%) | 실재하는 여러 이름이 섞임 | python-dateutils (실제는 python-dateutil) | 진짜와 매우 흡사하여 구분이 어려움 |
| 타이포형 (약 13%) | 1~2글자만 어긋남 | reqeusts (실제는 requests) | 자신의 오타라고 착각함 |
| 완전한 창작 (약 51%) | 어디에도 없는 이름을 만듦 | super-fast-jsonx | "편리해 보여서" 믿어버림 |
절반 이상이 "완전한 창작"입니다. 즉, 실재 여부만 확인해도 상당한 비율을 그 자리에서 걸러낼 수 있습니다. 반대로 말하면, 체크하지 않으면 절반은 그냥 통과한다는 뜻이기도 합니다.
무서운 구조를 알았으니, 그럼 실제로 어디에서 문제가 발생하는지 살펴보겠습니다. 개발 흐름(Workflow) 중에서 위험한 포인트를 솔직하게 나열해 두겠습니다. 적의 얼굴을 알고 있으면 지켜야 할 곳이 결정됩니다.
- 인간의 복사 붙여넣기 실행… AI의 답변에 포함된
pip install ○○를 이름을 확인하지 않고 그대로 터미널에 입력. 가장 고전적이고, 가장 빈번함. - AI 에이전트의 자동 설치… 최근에는 AI가 스스로 명령어를 실행합니다. 인간이 보고 있지 않은 곳에서
install까지 실행되면, 환각 이름이 그대로 입력됩니다. - 의존성 파일의 AI 자동 생성… "
requirements.txt좀 만들어줘"라고 요청하면, AI가 실재하지 않는 의존성을 섞어서 작성할 때가 있습니다. 리뷰를 거치지 않고pip install -r을 실행하면 그대로 통과됩니다.
공통점은, 실재 여부와 출처를 확인하는 단계가 어디에도 끼어 있지 않다는 것입니다. 반대로 말하면, 이 단계를 어딘가에 하나만 넣어도 공격 경로를 상당히 좁힐 수 있습니다. 그럼 그 단계를 어떻게 설계할까요? 여기서부터가 본론입니다.
가장 중요한 원칙을 먼저 세우겠습니다.
AI는 "이름을 제안하는 역할". 실재 여부와 출처를 확인하는 것은 인간과 CI의 역할.
이 경계선이 오늘 만들 설계의 뼈대입니다. AI에게 "이름을 내놓지 마"라고 하는 것은 무리가 있고 아깝습니다. AI는 오히려 마음껏 후보를 내놓아도 좋습니다. 다만, 그 후보를 "믿고 실행에 옮기는" 단계만큼은 인간과 자동 체크가 담당해야 합니다. 이 둘을 섞지 않는 것이 핵심입니다.
"AI가 대단하다"에서 끝내지 않고, 어디를 맡기고 어디를 직접 통제할 것인가. 구체적으로 코드로 구현해 봅시다.
먼저 기본 단계입니다. 패키지 이름을 전달하면 PyPI에 실재하는지, 그리고 "너무 갓 태어난 것은 아닌지"까지 확인하여 판정합니다. PyPI는 https://pypi.org/pypi/{이름}/json
를 호출하면, 존재할 경우 정보가 반환되고 존재하지 않으면 404가 반환되므로 이를 사용합니다.
import sys
import json
import urllib.request
...
포인트는 "실재하는가"뿐만 아니라 "너무 최신은 아닌가"까지 확인하고 있다는 점입니다. slopsquatting(슬롭스쿼팅) 가짜 패키지는 대개 "아주 최근에 등록되었고" "버전이 딱 하나"뿐입니다. 따라서 실재하더라도 갓 태어난 패키지라면 일단 사람의 눈을 거치도록 합니다. 이 부분이 핵심입니다. STOP
일 때는 종료 코드(exit code) 1을 반환하므로, 나중에 CI(지속적 통합)에서 그대로 사용할 수 있습니다.
Node.js 측도 같은 발상으로 가봅시다. npm은 https://registry.npmjs.org/{이름}
에서 정보를 가져올 수 있습니다. 여기서는 **allowlist (팀에서 승인된 표준 목록)**와 대조하여, 모르는 의존성만 사람의 리뷰로 넘기는 방식을 취합니다.
// verify-npm-deps.mjs — AI가 추가한 의존성을 실재성·출처·허가 목록으로 판정
import { readFile } from "node:fs/promises";
// 팀에서 "이것은 사용해도 OK"라고 합의한 표준 목록만 넣음
...
allowlist 방식의 장점은 "아는 것은 통과시키고, 모르는 것은 막는다"라는 기본 거부(Default Deny)의 발상을 담고 있다는 것입니다. 새로운 의존성을 추가할 때마다 사람이 잠시 멈추게 됩니다. 이 "잠시"가 내일의 자신을 지킵니다.
개인의 마음가짐만으로는 바쁜 날에 놓칠 수 있습니다. 그래서 시스템으로 구축합니다. 풀 리퀘스트(Pull Request)에서 새로 추가된 의존성만 추출하여, 코드 1로 검증하고, STOP
이 나오면 머지(Merge)를 중단하는 GitHub Actions입니다.
# .github/workflows/dep-guard.yml
name: dependency-guard
on: [pull_request]
...
verify_pypi.py는 방금 본 코드 1을 그대로 두기만 하면 됩니다. 차분(diff) 중의 "신규 추가분"만 보는 것이 요령입니다. 기존 의존성을 매번 전부 체크하면 무겁고 번거롭습니다. "지금 늘어난 것만"에 집중하면 실제 운영 환경에서도 원활하게 돌아갑니다. 처음에는 차단하지 않고 경고만 하다가, 익숙해지면 exit 1로 중단시키는 정도의 온도감으로 시작해도 충분하다고 생각합니다.
마지막으로, 애초에 "같은 이름이라도 내용물이 바뀌는" 사고를 방지하는 해시 고정(Hash pinning)입니다. 이는 slopsquatting뿐만 아니라 의존성 변조 전반에 효과적입니다.
# pip: 해시를 포함하여 고정하고, 내용물 교체를 차단함
pip install pip-tools
pip-compile --generate-hashes requirements.in # requirements.txt에 해시를 삽입
...
--ignore-scripts는 은근히 중요합니다. 악의적인 패키지는 postinstall 단계에서 외부와 통신하기도 하므로, 우선 스크립트를 실행하지 않고 설치한 뒤 내용을 확인한다는 완충 장치가 효과를 발휘합니다.
코드로 문지기를 만드는 동시에, AI와의 상호작용 자체도 안전한 방향으로 맞춰둡시다. 여기서 프롬프트(Prompt)의 차례입니다.
당신은 시니어 엔지니어입니다. 다음 요구사항에 맞는 코드를 작성해 주세요.
의존성에 관한 제약 사항 (반드시 준수):
- 외부 라이브러리를 import / install 하는 경우, 실재가 확실한 표준 라이브러리만 사용할 것
...
AI에게 "URL을 병기해줘"라고 말하면, 자신이 내뱉은 이름과 한 번 마주하는 단계가 끼어듭니다. 완벽하지는 않지만, 아무런 준비 없이 복사해서 붙여넣는 것보다는 훨씬 낫습니다.
다음 PR 차분에서 "새로 추가된 의존성 패키지"를 모두 추출하여, 하나씩 트리아지(Triage)해 주세요.
출력 (Markdown 표):
| 패키지 | 추가 이유 (차분에서 추측) | 환각/typosquat 의심 | 대체안 (표준 라이브러리/기존 의존성) | 사람의 확인이 필요한가 |
...
다음 의존성 목록을 감사하고, 리스크가 높은 순서대로 정렬된 보고서를 작성해 주세요.
각 패키지를 다음 관점에서 표로 작성하세요:
| 패키지 | 공개 후 경과 시간 | 주간 다운로드 규모 추정치 | 메인테이너 수 추정치 | 알려진 우려 사항 | 리스크 (고/중/저) |
...
세 가지 방법의 공통된 사상은, AI에게 판단하게 하지 않고, AI에게는 "재료를 나열하게만" 하는 것입니다. 최종 판단은 인간이 쥐어야 합니다. AI는 표를 채우는 역할, 인간은 의미를 결정하는 역할. 역할을 섞지 않는 것이 요령입니다.
| 시그널 | 안심에 가까움 | 주의에 가까움 | 확인 방법 |
|---|---|---|---|
| 실재 여부 | 레지스트리에 있음 | 404가 반환됨 | PyPI / npm의 JSON API |
| ... |
단 하나만으로 흑백을 가리지 않는 것이 요령입니다. "새롭다"는 것만으로는 좋은 신작일 수도 있습니다. 하지만 "새로움 × 낮은 다운로드 수(DL) × 메인테이너 1명 × 기존 패키지와 유사한 이름"이 겹친다면, 그 지점에서는 멈춰 서야 합니다. 시그널은 곱셈으로 확인합니다.
| 공정 | AI에게 맡김 | 인간 / CI가 담당 |
|---|---|---|
| 이름 제안 | ◎ 후보를 제시함 | — |
| ... | ||
| 증상 | 위험한 이유 | 대응 |
| --- | --- | --- |
| 레지스트리 404 | 환각(Hallucination) 또는 미등록 | 설치(install)하지 말 것. 이름을 의심할 것 |
| ... | --ignore-scripts. 내용물을 확인 |
갑자기 전부 다 할 필요는 없습니다. 핵심은 "내일의 내가 조금이라도 도움을 받을 수 있는 작은 한 걸음"입니다.
- 현재 진행 중인 프로젝트의 새로운 의존성(Dependency)을 딱 하나만 골라 PyPI나 npm 페이지를 열고, 실재 여부와 공개일을 직접 눈으로 확인해 보기 (30초) - 코드 1 또는 코드 2를 저장하고, 가지고 있는 의존성 이름을 전달하여 한 번 돌려보기
- AI 생성 프롬프트에 프롬프트 1의 "검증 테이블을 붙여줘"를 추가하기
- CI에
dep-guard를 하나 추가하기 (코드 3). 처음에는 차단하지 않고 경고(Warning)만 주는 것으로도 충분합니다.
이 네 가지 중 오늘은 하나만 해도 된다고 생각합니다. 하나만 해두어도, 내일의 내가 "오, 확인해 둬서 고마워요!"라고 말해줄 것입니다. 그것만으로 충분합니다.
여기까지 읽어주셔서 감사합니다.
제가 이 이야기를 통해 가장 전달하고 싶었던 것은 "AI는 위험하니까 쓰지 마라"가 아닙니다. 오히려 반대로, AI에게 점점 더 많이 맡기고 싶기 때문에, 맡기는 범위의 바깥쪽에 조용한 울타리를 하나 쳐두자는 이야기였습니다.
의존성을 하나 확인하는 것은 단 30초 정도의 수수한 작업입니다. 하지만 그 30초를 게을리한 미래에 기다리고 있는 것은, 운영 환경(Production)에서 정보가 유출되거나 심야에 인시던트 대응(Incident Response)으로 얼굴이 창백해진 자신일지도 모릅니다. 만약 그렇다면, 오늘의 30초는 비난받으며 마지못해 하는 작업이 아니라, 미래의 자신에게 살며시 건네는 선물인 것입니다.
저는 항상 선택에 망설여질 때 "이것이 내일의 내가 고맙다고 말해줄 쪽은 어느 쪽일까"라고 생각하려고 노력합니다. 의존성 검증은 바로 그것입니다. 화려함은 없지만, 확실히 내일의 나를 도와줄 배려 깊은 한 수라고 생각합니다.
게다가, 만약 스스로 앱이나 서비스를 만들어 세상에 내놓는다면, "그 사람이 만든 것이라면 안심하고 사용할 수 있다"라는 신뢰야말로 가장 큰 자산이 됩니다. 의존성의 정체를 조용히 관리해 두는 것은 그 신뢰의 토대 그 자체입니다. 멋진 프로덕트는 이런 수수한 밑바탕으로부터 만들어진다고 믿습니다.
AI를 멈추는 것이 아니라, AI에게 안심하고 등을 맡길 수 있는 울타리를 만드는 것. 오늘의 작은 검증 하나부터 가볍게 시작해 보지 않으시겠습니까?
내일의 자신에게, 고마움을 전하며.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기