주니어 프론트엔드 개발자가 침해 직후 로컬 레포지토리와 패키지 설정에서 털린 흔적을 찾는 법
보안 사고 소식을 접한 뒤 밤늦게 구글링만 하고 앉아있을 시간이 없다. 2026년 5월 악성 비주얼 스튜디오 코드 확장 프로그램이 배포되면서 개발자 머신이 털렸고 사내 소스 코드 레포지토리 약 3,800개가 그대로 유출됐다. 이 판국에 무작정 코드를 지우거나 랩탑을 포맷하면 포렌식 증거가 다 날아가서 원인도 못 찾는다. 패키지 매니저 실행 로그, 전역 CLI 타임스탬프, 현재 떠 있는 프로세스 권한을 직접 뒤져서 뚫렸는지 확인하자.
10분 만에 로컬 개발 환경 털렸는지 자가 진단하기
패키지 매니저는 설치 과정에서 시스템 캐시 디렉터리에 트랜잭션 기록을 남긴다. npm은 npm config get cache 경로 안의 _logs 폴더에 디버그 로그를 뱉고 pnpm은 pnpm store path에 아티팩트를 처박아 둔다. 악성 패키지는 설치 순간 실행되는 스크립트를 통해 환경 변수나 자격 증명을 밖으로 빼돌리므로 라이프사이클 실행 내역부터 까봐야 한다.
최근 14일 치 로그를 뒤져서 무단 패키지 추가나 외부 스크립트 실행 여부를 잡는 명령어는 다음과 같다. 터미널을 켜고 아래 명령어를 그대로 친다.
NPM_CACHE_DIR=$(npm config get cache)
find $NPM_CACHE_DIR/_logs/ -type f -mtime -14 -exec grep -Hn "lifecycle" {} +
결과 화면에서 내가 의도한 적 없는 preinstall이나 postinstall 훅이 돌았는지 본다. 3분이면 끝난다. 이상한 외부 URL 통신 기록이 없다면 패키지 레벨의 직접 오염 공포에서는 일단 숨을 돌려도 된다.
공격자는 랩탑 안에 살아서 남기려고 글로벌 CLI 도구나 npx 캐시에 악성 바이너리를 박아 넣는다. 전역 설치 패키지 목록을 뽑고 바이너리 폴더 파일 생성일과 수정일자를 시스템 로그와 대조해 무결성을 확인한다.
npm list -g --depth=0 --json
ls -lact $(npm config get prefix)/bin/
수정 시점이 엉뚱한 시간대와 겹치면 아래 명령어로 해시값을 뽑아서 대조한다.
shasum -a 256 $(npm config get prefix)/bin/
패키지 설치 스크립트는 개발자 계정 권한 그대로 돈다. 백그라운드에 돌아가는 데몬 프로세스를 끊어야 한다.
ps aux | grep -E "node|npm|pnpm|bun" | grep -v grep
lsof -i -P -n | grep -E "node|npm|pnpm"
의심스러운 프로세스가 외부 C2 서버와 통신하고 있다면 당장 잡아서 죽이자.
kill -9 [PID]
npm config set ignore-scripts true
패키지 설정 파일과 레지스트리 주소 오염 검사하기
공급망 공격의 단골 패턴은 설정 파일을 건드려 레지스트리를 가로채는 짓이다. 공격자는 프로젝트 안의 .npmrc 파일이나 전역 설정에 악성 미러 서버 주소를 박아 넣는다. 락파일 내부의 다운로드 URL 경로를 바꿔치기해 실제 설치할 때 악성 tarball을 받게 만들므로 설정값을 싹 다 뒤져야 한다.
로컬 설정과 전역 설정에 미인가 오버라이드가 박혀 있는지 확인하는 절차는 이렇다. 먼저 설정 바인딩 목록을 뽑는다.
npm config list
pnpm config list
다음으로 사내 프라이빗 레지스트리 스코프가 제대로 잡혀 있는지 확인한다.
npm config get @company:registry
사용자 홈과 프로젝트 루트 설정 파일에 외부 미러 주소가 하드코딩되어 있는지도 직접 검색한다.
grep -Rn "registry" ~/.npmrc ./.npmrc
이 세 단계를 거치면 이상한 미러 주소가 등록되었는지 5분 안에 판별할 수 있다.
package-lock.json과 pnpm-lock.yaml 파일은 의존성 다운로드 원천 URL을 기록하므로 정규표현식으로 오염 여부를 털어야 한다.
grep -E '"resolved": "https?://' package-lock.json | grep -vE 'registry.npmjs.org|registry.corp.example'
pnpm을 쓴다면 아래 명령어를 입력한다.
grep -E 'resolution: {tarball:' pnpm-lock.yaml | grep -vE 'registry.npmjs.org|registry.corp.example'
오염된 락파일이 나왔다면 다 지우고 새로 짜야 한다.
rm -rf node_modules package-lock.json pnpm-lock.yaml
npm cache clean --force
pnpm store prune
npm config set registry https://registry.npmjs.org/
npm ci --ignore-scripts
라이프사이클 스크립트 실행을 무시하고 불변 상태의 의존성 트리를 재구성하면 순정 상태의 환경을 확보할 수 있다.
SSH 키와 API 토큰 권한 깎아내리기
악성 바이너리는 리눅스와 맥 환경에서 브라우저 비밀번호 저장소를 털고 환경 변수에 박힌 AI API 키, AWS Access Key, SSH 키를 싹 다 긁어간다. 터미널 환경에서 자격 증명이 노출되었는지 지금 당장 점검해야 한다.
ls -la ~/.ssh/
env | grep -E 'TOKEN|KEY|SECRET|AUTH|AWS|GITHUB|OPENAI|ANTHROPIC'
grep -E '(ghp_[A-Za-z0-9]{36}|AKIA[0-9A-Z]{16}|bearer)' ~/.zsh_history ~/.bash_history
평문으로 노출된 키가 나오면 눈감아주지 말고 즉시 폐기한다. GitHub CLI로 로그인된 토큰 스코프도 확인한다.
gh auth status
모든 레포지토리 접근 권한이 들어간 Classic 토큰은 당장 날려버려라. PAT를 다시 만들 때는 특정 레포지토리만 지정하고 읽기 권한도 최소 범위로만 묶어둔다.
로컬 호스트와 개발 프로세스를 격리하려면 DevContainer 구조를 써야 한다. 프로젝트 루트에 .devcontainer/devcontainer.json 파일을 파고 아래처럼 이미지를 설정한다.
{
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
"postCreateCommand": "npm ci --ignore-scripts"
}
이렇게 컨테이너를 띄우면 패키지 설치와 빌드가 랩탑 본체의 시스템 자격 증명과 완전히 격리된 상태로 돌기 때문에 사내 자산 유출 경로를 원천 차단할 수 있다.