RicoCheesethe studio log · v2.0
Live · KRRead posts
목록으로
뉴스PUBLISHED · 2026년 7월 28일·26 MIN READ

Slopsquatting: AI 환각을 무기로 삼는 새로운 공급망 공격

AI가 존재하지 않는 패키지를 지어내면 공격자가 그 이름을 선점해 기다린다. 오타가 아니라 모델의 환각을 노리는 slopsquatting의 공격 흐름과 실제로 통하는 방어책을 정리했다.

#security#ai#devops#opensource#news
Slopsquatting: The Supply Chain Attack That Weaponizes AI Hallucinations

개요 #

코딩 어시스턴트가 이런 명령을 알려줬다고 해보자.

untitled
bash
pip install requests-oauth2-helper

이름만 보면 완벽하다. requests에 OAuth2 토큰을 붙이는 깔끔한 방법을 물었더니 딱 그 일을 할 것 같은 이름이 돌아왔다. 대소문자도, 하이픈 규칙도 파이썬 생태계 관습 그대로다. 그래서 설치하고, 테스트가 통과하고, 다음 작업으로 넘어간다.

문제는 이 패키지가 모델이 학습할 당시엔 세상에 없었다는 점이다. 모델이 지어낸 이름이다. 그리고 누군가 그 순간을 노리고 있었다면, 그 이름은 더 이상 비어 있지 않다.

이것이 slopsquatting이다. 타이포스쿼팅(typosquatting)이 사람의 오타를 노렸다면, slopsquatting은 그 자리에 기계의 환각을 집어넣는다. 파이썬 소프트웨어 재단의 Developer-in-Residence인 Seth Larson이 이름 붙였고, 2025년 Ecosyste.ms의 Andrew Nesbitt가 널리 알렸다. 발상은 단순하지만 결과는 고약하다. 이제 reqeusts처럼 손가락이 미끄러질 필요조차 없다. AI가 확신에 찬 얼굴로 가짜 패키지를 건네고, 개발자는 자기 오타보다 그걸 더 믿는다.

타이포스쿼팅은 실수를 기다렸지만, 이건 다르다 #

타이포스쿼팅은 오래된 수법이다. express 옆에 expres를, python-dateutil 옆에 python-dateutl을 등록해 두고 누군가 손가락이 미끄러지길 기다린다. 통하긴 하지만 확률 싸움이다. 대부분은 express를 정확히 친다. 공격자는 실수하는 소수를 낚으려 애쓴다.

slopsquatting은 사람의 실수라는 전제 자체를 지운다. 개발자는 이름을 한 글자도 틀리지 않고 정확히 입력한다. 지금 이 순간만큼은 자기 자신보다 더 신뢰하는 대상, 즉 방금 주변 코드를 짜준 어시스턴트에서 그대로 복사하기 때문이다. 실수는 이미 상류에서, 모델이 저질렀고 사람은 그걸 충실히 재현할 뿐이다.

여기서 결정적인 변화가 생긴다. 타이포스쿼팅에서 공격자는 개발자의 손가락이 어디로 미끄러질지 추측한다. slopsquatting에서는 추측조차 하지 않는다. 모델이 실제로 무엇을 뱉어내는지 대규모로 관찰하고, 자주 등장하는 이름을 등록한다. 타깃 선정을 모델이 대신 해주는 셈이다.

왜 통하는가: 환각은 흔하고, 게다가 반복된다 #

모델의 환각이 드물고 무작위였다면 slopsquatting은 각주 수준의 얘기로 끝났을 것이다. 가짜 이름을 등록하고 하염없이 기다려봐야 아무도 안 걸린다. 이게 실제 위협인 이유는 그 두 가정이 모두 틀렸기 때문이다.

먼저 규모다. USENIX Security 2025에 발표된 연구 We Have a Package for You! 는 16개 LLM으로 파이썬과 자바스크립트 코드 샘플 57만 6천 개를 생성하고, 모델이 추천한 패키지를 전부 확인했다. 추천 패키지의 19.7%가 존재하지 않았다. 벤치마크 언저리의 반올림 오차가 아니라, 다섯 개 중 하나꼴이다. 서로 다른 환각 패키지 이름만 20만 5,474개가 집계됐다. 상용 모델은 평균 5.2% 이상으로 그나마 나았고 오픈소스 모델은 21.7% 이상으로 더 나빴지만, 깨끗한 모델은 하나도 없었다.

환각 하나하나가 매번 다른 눈송이라면 규모가 크더라도 감당할 만하다. 공격자가 20만 개를 다 등록할 수도 없고, 그중 어떤 게 다시 나타날지 알 방법도 없으니까. 그런데 진짜 무서운 발견은 환각이 재현된다는 점이다. 연구진은 가짜 패키지를 만들어낸 프롬프트 500개를 골라 각각 열 번씩 더 돌렸다. 환각 패키지의 43%가 매번 빠짐없이 다시 나왔다. 58%는 두 번 이상의 실행에서 등장했다. 한 번도 다시 안 나온 건 39%뿐이었다.

이 숫자를 곱씹어 보자. 이 조작된 이름의 거의 절반이 노이즈가 아니라 안정적인 모델 행동이다. 경제성이 완전히 뒤집힌다. 공격자는 전부 등록할 필요가 없다. 모델 출력을 채굴해 반복되는 이름만 추리고, 그중 끈질긴 것들만 등록하면 된다. 재현성이 곧 정찰이다. 미래의 개발자가 어떤 가짜 이름을 건네받을지, 모델이 몇 번이고 알려준다.

경고 이름이 진짜 패키지처럼 생길 필요조차 없다. 같은 연구가 레벤슈타인 거리로 분석해 보니, 환각 이름 중 실제 패키지의 단순 오타에 해당하는 건 13%뿐이었다. 약 38%는 중간 정도로 비슷했고, 거의 절반은 완전히 딴판이었다. 완전히 지어낸 이름이지만 주변 코드 맥락에서는 여전히 그럴듯했다. 바로 이 마지막 부류가 타이포스쿼트 탐지를 유유히 통과한다. 이유는 뒤에서 다시 다룬다.

공격 흐름, 한 단계씩 #

이건 이론이 아니다. 깔끔하고 반복 가능한 순서이고, 모든 고리가 평범한 근무일에 이미 지켜본 장면들이다.

1. 모델이 import를 제안한다. 기능을 요청하면 어시스턴트가 코드를 짜면서 문제 형태에 맞는 의존성을 끌어온다. 그러면서 이름 하나를 내놓는다. 지어낸 이름이지만 문법적으로는 멀쩡하다. 생태계 관습에 맞고, 대소문자가 자연스럽고, "이런 게 있을 법한데" 싶은 분위기를 풍긴다.

2. 이름이 그럴듯해서 믿는다. 여기가 하중을 견디는 지점이고, 기술이 아니라 심리의 문제다. 확신에 찬 AI 출력은 확신에 찬 시니어 엔지니어의 PR과 똑같이 경계를 슬쩍 통과한다. 이름이 관용적이고 위험을 알리는 패턴이 없다. 개발자는 로직을 검토하지, 네 단어짜리 패키지 이름이 진짜 있는지 감사하지 않는다. 누가 그런 걸 일일이 확인하겠는가.

3. 설치가 코드를 실행한다. npm과 pip에서는 패키지 설치만으로 스크립트가 자동 실행될 수 있다. postinstall 훅, setup.py 빌드 단계 같은 것들이다. 공격자의 페이로드는 개발자가 뭘 import하거나 함수를 호출해 줄 필요가 없다. install이 끝난 시점에 코드는 이미 돌아갔다.

4. 인증 정보가 빠져나간다. 페이로드는 빌드 에이전트에 늘 널려 있는 것들을 읽는다. 환경 변수, ~/.aws/credentials, ~/.npmrc 토큰, GITHUB_TOKEN, .env 파일. 그리고 공격자가 지정한 엔드포인트로 POST 한다. 밖에서 보면 설치 중 메타데이터를 받아오는 패키지처럼 보인다. 안에서 보면, 방금 당신의 CI가 낯선 사람에게 열쇠를 통째로 넘겼다.

slopsquatting 공격 흐름을 다섯 단계로 나타낸 다이어그램: 모델이 조작된 import requests-oauth2-helper를 제안하고, 개발자가 그�럴듯한 이름을 믿고, pip 또는 npm install이 postinstall을 실행하고, postinstall 스크립트가 동작해 토큰과 인증 정보를 공격자에게 유출한다.

전 과정이 섬뜩할 만큼 조용하다. 익스플로잇도, CVE도, 정교한 메모리 손상도 없다. 개발자가 이름 하나를 믿고, 하루에 쉰 번씩 치던 그 설치 명령을 그대로 쳤을 뿐이다. 그게 공격의 전부다.

그리고 사람이 신뢰 단계에서 넘어간다는 건 가정이 아니다. Lasso Security의 보안 연구자 Bar Lanyado는 모델들이 huggingface-cli라는 파이썬 패키지를 반복해서 환각하는 걸 발견했다. 그는 실험 삼아 PyPI에 그 이름으로 빈 패키지를 등록했다. 이후 석 달 동안 실제 다운로드가 1만 5천 건을 넘겼고, 알리바바의 GraphTranslator 프로젝트는 README에 pip install huggingface-cli를 추천하기까지 했다. 진짜 도구는 pip install -U "huggingface_hub[cli]"로 설치하는데도 말이다. Lanyado의 패키지는 일부러 무해하게 만든 것이었다. slopsquatter의 것은 그렇지 않다.

같은 발상, 네 생태계, 서로 다른 피해 반경 #

간단히 말하면 "자바스크립트, PHP, Go 어디든 통한다"이다. 정확히 말하면, 각 생태계가 공격자에게 내주는 여유가 제각각이고, 그 차이를 알면 방어 예산을 어디에 쓸지가 보인다.

npm — 공격자에게 가장 많이 내준다 #

라이프사이클 스크립트(preinstall, install, postinstall)가 npm install 때 자동으로 돈다. 이게 고전적인 벡터이고, 생태계가 마침내 움직이는 이유이기도 하다. pnpm v10은 2025년 초 의존성 라이프사이클 스크립트를 기본으로 비활성화했고, npm도 뒤따랐다. 2026년 6월 발표된 npm v12는 자동 스크립트 실행을 기본으로 끈다. 그 버전으로 올리기 전까지 postinstall은 CI를 겨눈 장전된 총이다.

package.json (악성 의존성):

untitled
json
{
  "name": "requests-oauth2-helper",
  "version": "1.0.3",
  "scripts": {
    "postinstall": "node ./collect.js"
  }
}

pip — 바로 뒤를 따른다 #

소스 배포판(sdist)은 설치 시점에 setup.py를 실행해 메타데이터를 계산하고 빌드한다. 즉 pip install에 임의 코드 실행이 딸려 온다. 반면 휠(.whl)은 설치 시점 코드를 같은 방식으로 돌리지 않는다. --only-binary가 그냥 좋은 옵션이 아니라 실질적인 방어 수단인 이유다.

untitled
bash
# 소스 빌드를 거부하고 미리 빌드된 휠만 받는다
pip install --only-binary :all: requests-oauth2-helper

Composer — 설계상 셋 중 가장 안전하다 #

Composer는 루트 패키지의 composer.json에 정의된 스크립트만 실행한다. 의존성 자체의 scripts 블록은 무시된다. 그래서 slopsquat된 Composer 패키지는 npm이 postinstall을 떠넘기듯 post-install-cmd를 자동으로 떠넘길 수 없다. 단 하나의 예외는 플러그인이다. 악성 Composer 플러그인은 설치 이벤트에 훅을 걸 수 있어서, 신뢰할 수 없는 트리에는 composer install --no-plugins --no-scripts가 존재한다. 페이로드는 당신 코드가 실제로 그걸 호출할 때까지 기다려야 하는데, 이는 "설치하면 돈다"보다 확연히 높은 문턱이다.

Go — 설치 스크립트 자체가 없어 흐름이 바뀐다 #

go getgo build는 의존성을 추가하는 순간에 임의의 설정 코드를 돌리지 않는다. slopsquat된 Go 모듈의 악성 코드는 프로그램이 그걸 처음 실행할 때, 즉 import 시 패키지 init() 함수를 통해서나 go test 도중에 돈다. 나중이긴 해도, 결국은 돈다.

Go에는 모듈 프록시라는 고유한 반전이 하나 더 있다. Socket은 BoltDB의 백도어 타이포스쿼트 github.com/boltdb-go/bolt를 찾아냈다. 2021년 11월에 업로드돼 Go 모듈 미러에 캐시된 뒤, Git 태그를 깨끗한 코드로 다시 썼는데도 프록시는 캐시된 악성 버전을 계속 서빙했다. 3년 넘게 발각되지 않았다. 빌드를 재현 가능하게 만들려던 캐싱이, 오염된 버전을 질기게 살려두는 역할까지 한 것이다.

정리하면 "한 공격, 세 언어"가 아니다. npm과 pip는 설치 시점에 터지고, Composer는 플러그인이나 호출을 기다리며, Go는 실행을 기다렸다가 나쁜 버전을 영영 기억한다. slopsquatter는 가장 이르고 조용한 방아쇠를 주는 생태계를 고른다.

왜 기존 공급망 방어가 이걸 못 잡나 #

불편한 대목이 여기다. 이미 돌리고 있는 통제 대부분은 다른 위협 모델을 겨냥해 만들어졌고, slopsquatting은 그 사이 틈으로 빠져나간다.

락파일은 첫 설치 이후에만 도움이 된다. package-lock.json이나 composer.lock은 정확한 버전과 해시를 고정해 의존성이 몰래 바뀌지 못하게 한다. 이미 검증하고 고정한 패키지에는 훌륭하다. 하지만 slopsquatting은 완전히 새로운 이름의 설치를 노린다. 락파일엔 아직 아무것도 없다. 그 물건을 설치한 적이 없으니까. AI가 30초 전에 방금 제안한 것이다. 락파일은 당신이 처음 받은 악성 버전을 충실히 기록하고, 이제 그게 고정된다. 락파일은 연속성을 지키지, 첫 접촉을 지키지 않는다.

스캐너는 이미 알려진 악성을 찾지, 이건 처음 보는 것이다. 취약점 스캐너나 악성코드 피드는 누군가 이미 플래그를 단 패키지와 대조한다. 어제 등록됐고, 이번 분기에야 반복되기 시작한 환각을 겨냥한 slopsquat 패키지에는 CVE도, 권고문도, 평판도, 이력도 없다. 없어서 깨끗해 보인다. 스캐너가 고장 난 게 아니다. 다른 질문에 답하고 있을 뿐이다. "이게 알려진 위협인가"는 "이게 사람이 고른 진짜 패키지인가"와 다른 물음이다.

타이포스쿼트 탐지는 편집 거리로 판단하는데, 이 이름들의 절반은 아무것도 안 닮았다. 타이포스쿼팅의 표준 방어는 문자열 유사도를 잰다. expresexpress로 바꾸는 데 몇 번 편집이 필요한가? 아슬아슬하게 비슷한 걸 플래그한다. 하지만 앞서 본 레벤슈타인 결과를 떠올려 보자. 환각 이름의 거의 절반은 어떤 실제 패키지와도 완전히 딴판이었다. requests-oauth2-helper는 진짜 이름에서 한 번 편집한 거리에 있지 않다. 기존 이름의 변형조차 아니다. 그저 그럴듯하게 들리는, 갓 지어낸 발명품이다. 거리를 잴 이웃이 없으니 탐지기가 걸고넘어질 게 없다.

셋을 겹쳐 보면 틈의 모양이 드러난다. 이 방어들은 모두 악성 패키지가 바뀌었거나(락파일), 예전에 플래그됐거나(스캐너), 진짜 뭔가를 흉내 낸다(타이포스쿼트 탐지)고 가정한다. slopsquatting은 그중 무엇도 아니다. 처음 보는 진짜 새 이름이고, 아무것도 안 닮았으며, 확신에 찬 목소리로 기계가 골라준 것이다. 방어가 약한 게 아니다. 다른 곳을 겨누고 있을 뿐이다.

실제로 통하는 것: 믿음만으로 이름을 저장소에 들이지 마라 #

다행히 해법은 특이할 게 없다. 어떤 의존성이든 마땅히 적용해야 할 본능을, 지나치게 믿기 시작한 출처에 그대로 적용하는 것뿐이다. 새로운 제품 카테고리가 필요한 게 아니다. AI의 패키지 제안을 출처(citation)처럼 대접하는 습관을 그만두면 된다.

설치 전에 패키지가 실재하고 관리되는지 확인한다 #

모델이 건넨 걸 설치하기 전에 찾아본다. 레지스트리에 있는가? 다운로드 수와 버전 수는? 마지막 릴리스는 언제고 누가 배포하며, 뒤에 사람이 분명히 관리하는 저장소가 있는가? slopsquat 패키지는 보통 며칠밖에 안 됐고, 다운로드 수가 수상하게 딱 떨어지며, 이력이 없다. 30초만 들여다봐도 대부분 걸러진다.

untitled
bash
# npm: 존재하나, 누가 소유하나, 얼마나 오래됐나?
npm view requests-oauth2-helper

# PyPI: 프로젝트 페이지, 릴리스 이력, 관리자 확인
pip index versions requests-oauth2-helper

첫 명령이 404를 뱉으면, 모델이 숨은 보석을 찾아준 게 아니다. 지어낸 것이다.

설치 스크립트를 기본으로 끈다 #

이 하나의 통제가 공격 흐름에서 가장 시끄러운 버전을 무력화한다. 자동 실행을 꺼두고, 진짜 필요한 빌드 단계는 기본값이 아니라 명시적 예외로 둔다.

npm:

untitled
bash
# 라이프사이클 스크립트를 전역으로 끄고, 꼭 필요한 패키지만 허용 목록에 둔다
npm config set ignore-scripts true

pnpm:

untitled
bash
# pnpm v10+ 는 의존성 스크립트를 기본으로 막는다.
# 실제로 신뢰하는 것만 승인한다:
pnpm approve-builds

pip:

untitled
bash
# 미리 빌드된 휠을 선호해 setup.py 가 설치 시점에 안 돌게 한다
pip install --only-binary :all: <package>

composer:

untitled
bash
# 신뢰할 수 없는 트리? 플러그인도, 스크립트도 없이.
composer install --no-plugins --no-scripts

설치 스크립트를 정당하게 쓰는 npm 패키지는 약 2%뿐이다. 생태계가 "끄는 게 온당한 기본값"이라고 결론 내린 이유가 바로 이것이다. 잃는 건 거의 없고, 페이로드가 노리던 문을 닫는다.

모든 걸 사설 레지스트리로 프록시하고 허용 목록을 둔다 #

빌드 에이전트가 공개 레지스트리에 손을 뻗어 모델이 이름 붙인 아무거나 받아올 수 있어선 안 된다. 앞에 프록시(Artifactory, Verdaccio, 사설 PyPI, Composer의 Satis)를 세우고, 검토를 거쳐 들어온 패키지만 허용한다. 그러면 환각 이름은 설치 가 아니라 설치 에 실패한다. 승인된 적이 없으니까. 이게 규모에 맞게 확장되는 통제다. 판단을 "개발자가 알아챘는가"에서 "목록에 있는가"로 옮기기 때문이다.

새 버전에 성숙 지연을 건다 #

pnpm v11은 minimumReleaseAge를 도입해 기본값을 1440분(24시간)으로 뒀다. 커뮤니티가 명백한 악성코드를 잡아낼 만큼 공개된 지 충분히 지나기 전까지는 그 버전 설치를 거부한다. 갓 나온 환각을 잡으려 달려드는 slopsquat 패키지야말로, 냉각 기간이 정확히 막아내기 좋은 대상이다.

그리고 모든 규칙 아래 깔린 단 하나 #

AI가 제안했다는 이유만으로 의존성을 저장소에 들이지 마라. AI의 패키지 제안은 가설이지 출처가 아니다. 처음 보는 계정이 단 Stack Overflow 답변처럼 대하라. 맞을 수도 있고, 확인할 가치가 있고, 보자마자 믿을 것은 아니다. 공격 전체가 2단계의 그 한순간, 잘못 놓인 신뢰에 매달려 있다. 이름을 확인하는 순간 공격은 발 디딜 곳을 잃는다.

불편한 진실은 모델이 이 짓을 곧 멈추지 않으리라는 것이다. 환각은 이 시스템이 텍스트를 생성하는 방식 자체의 속성이고, 가짜 패키지 이름은 안에서 보면 진짜와 똑같이 읽힌다. 그래서 짐은 install을 실행하는 사람, 바로 당신에게 있다. 어시스턴트가 방금 준 이름은 진짜 라이브러리일 수도, 진짜 라이브러리의 이름을 뒤집어쓴 공격자의 패키지일 수도 있다. 둘을 가려낼 유일한 방법은 설치 전에 들여다보는 것이고, slopsquatting의 핵심은 당신이 그러지 않으리라는 데 있다.


이 글은 위 출처를 바탕으로 한국 독자를 위해 재작성한 기사입니다. 원문의 사실과 수치에 근거하며, 별도의 견해를 포함하지 않습니다.