에디터로 열기 전, 모든 레포지토리에 로컬 LLM을 적용하는 이유
요약
악성 코드가 포함된 레포지토리로부터 개발자를 보호하기 위해, 에디터를 열기 전 로컬 LLM을 활용하여 정적 분석을 수행하는 보안 워크플로우를 제안합니다.
핵심 포인트
- 레포지토리 실행 전 로컬 LLM을 통한 정적 읽기(Static Reading) 수행
- git clone --no-checkout 또는 tarball 방식을 통한 비활성 상태 유지
- package.json의 라이프사이클 스크립트 및 lockfile 불일치 우선 검사
- 에디터 확장 프로그램의 자동 실행 위험성 경고
지난 5월, 한 "채용 담당자"가 Web3 직무를 위한 과제 프로젝트(take-home project)를 보냈습니다. 깔끔한 README, 그럴듯한 Next.js 구조, 실제 존재하는 것 같은 회사 이름까지 갖추고 있었죠. 하지만 빌드 도구(build tooling) 속에 숨겨진 postinstall 스크립트는 base64 블롭(blob)을 디코딩하여 하드코딩된 IP 주소로부터 두 번째 스테이지를 가져오도록 설계되어 있었습니다. 만약 제가 나머지 99%의 지원자들처럼 git clone을 하고, npm install을 실행한 뒤 에디터로 열었다면, 코드 한 줄을 읽기도 전에 제 컴퓨터에서는 정보 탈취형 악성코드(infostealer)가 실행되었을 것입니다.
그것이 마지막 사례도 아니었습니다. 이러한 가짜 채용 담당자를 이용한 미끼(lures)는 이제 하나의 산업이 되었으며, 페이로드(payload)는 거의 항상 src/ 디렉토리에 있지 않습니다. 대신 여러분이 대충 훑어보는 곳, 즉 라이프사이클 스크립트(lifecycle scripts), 설정 파일(config files), 혹은 이상한 함수가 하나 들어있는 "utils" 파일 등에 숨어 있습니다. 그래서 저는 기본 방식을 바꿨습니다. 이제 모든 알 수 없는 레포지토리는 에디터가 닿기 전에 로컬 LLM(local LLM)을 통한 분류(triage) 과정을 거칩니다. 코드 실행도, 설치도 없이 오직 정적 읽기(static reading)만 수행합니다.
워크플로우는 다음과 같습니다.
규칙 제0조: 레포지토리가 무엇도 실행하게 두지 마라
핵심은 레포지토리를 비활성(inert) 상태로 유지하는 것입니다. 파일을 확인하는 두 가지 안전한 방법은 다음과 같습니다:
# 옵션 1: 체크아웃 없이 클론하여 먼저 트리(tree)를 검사하기
git clone --no-checkout https://github.com/some-org/take-home-task.git
cd take-home-task
...
저는 tarball 방식을 선호합니다. 아무것도 실행할 수 없으며, quarantine/ 디렉토리에 압축을 풀면 스스로에게 엄격함을 유지할 수 있습니다. 또한 명시적으로 말해두자면, 작업을 자동으로 실행하는 플러그인이 설치된 에디터로 해당 폴더를 열지 마십시오. VS Code는 워크스페이스 설정(workspace settings)이나 실행 구성(launch configs)을 기꺼이 실행하며, 일부 확장 프로그램(extensions)은 호의를 베푼다며 대신 npm install을 실행하기도 합니다. 파일은 cat, bat, 또는 아래의 LLM 파이프라인을 통해 읽으십시오.
1단계: 대부분의 페이로드를 잡아내는 지루한 검사들
AI를 개입시키기 전에, 세 가지 저렴한 검사만으로도 이러한 캠페인의 대다수를 잡아낼 수 있습니다:
# 라이프사이클 스크립트(Lifecycle scripts)는 제1의 전달 메커니즘입니다
cat package.json | jq '.scripts | with_entries(
select(.key | test("install|prepare|prepublish|postpack")))'
...
lockfile 불일치 체크는 사람들이 생각하는 것보다 더 중요합니다. 제가 분석한 여러 캠페인들은 깨끗해 보이는 lockfile과 지저분한 package.json을 함께 배포하거나, 스크립트 내에서만 타이포스쿼팅(typosquatted)된 패키지를 참조합니다. 바로 이러한 유형의 레포지토리를 스캔하는 도구인 Argus Lens(lens.noctis.biz)를 구축했을 때, deps-missing-from-lockfile은 도구 전체에서 가장 신호 강도(signal)가 높은 체크 항목 중 하나로 나타났습니다.
2단계: 의심스러운 파일을 로컬 모델에 입력하기
정규표현식(Regex)은 후보군을 찾아줍니다. 판단은 모델이 제 역할을 하는 단계이며, 이때 반드시 로컬 모델(local model)을 사용해야 합니다. 왜냐하면 저는 때때로 비밀 유지 계약(NDA) 하에 있는 레포지토리를 분류하거나, URL 자체만으로도 내 기기 외부로 유출되기를 원치 않는 레포지토리를 처리하기 때문입니다.
저는 WSL2에서 Ollama를 실행하며, 두 가지 크기의 qwen2.5-coder를 사용합니다. 모든 항목을 빠르게 훑기 위한 1.5b 모델과, 작은 모델이 무언가를 감지했을 때 사용하는 7b 모델입니다. 프롬프트(prompt)는 채팅용이 아닌 분류기(classifier) 역할을 합니다:
triage_file() {
local file="$1"
ollama run qwen2.5-coder:7b <<EOF
...
그다음은 후보군에 대한 루프(loop)를 돌리는 것뿐입니다:
rg -l 'child_process|eval\(|Function\(|fromCharCode|atob|Buffer\.from' \
--glob '!node_modules' quarantine/ | while read -r f; do
echo "=== $f"
...
여기서 엄격한 출력 형식(strict output format)이 실제로 큰 역할을 합니다. 작은 모델들은 횡설수설하는 경향이 있는데, "첫 번째 줄에 판결(VERDICT)을 포함하여 답변하라"는 지시는 횡설수설하는 모델을 grep이나 스크립트로 처리할 수 있는 형태로 바꿔줍니다.
실제로 중요한 신호는 무엇인가
이 파이프라인을 통해 수십 개의 레포지토리를 처리하고(그리고 보안 작업을 위해 작은 모델에 프롬프팅하는 법을 많이 가르쳐준 저의 오픈 소스 계약 감사 도구인 spectr-ai를 구축하면서), 실제 페이로드(payload)와 노이즈를 구분하는 신호들을 다음과 같이 정리했습니다:
설치 시점의 실행 (Install-time execution). 합법적인 프로젝트는 네이티브 모듈 빌드 외에 postinstall을 필요로 하는 경우가 거의 없습니다. 네트워크에 접속하거나 문자열을 디코딩하는 postinstall은 유죄 판결이 거의 확실한 수준입니다.
매니페스트(manifest)에는 있지만 lockfile에는 없는 의존성 (Deps in manifest but not in lockfile). 위에서 다루었습니다. 이는 공격자가 사용자의 기기에서 의존성 해결(resolution)이 새로 이루어지기를 원한다는 것을 의미합니다.
인코딩된 블롭과 디코더. base64 문자열만으로는 종종 충분합니다 (내장 이미지, 테스트 더미 파일). 하지만 eval, new Function, 또는 child_process.exec의 범위 내에 있는 base64 문자열은 그렇지 않습니다.
환경 변수 수집(Env harvesting). process.env를 반복하거나, ~/.ssh, 브라우저 확장 프로그램 폴더, 또는 Local Storage/leveldb와 같은 지갑 데이터 디렉터리 방향으로 경로를 구축하는 행위입니다. take-home CRUD 앱이 MetaMask가 상태를 보관하는 위치를 알 필요가 있는 정직한 이유는 없습니다.
노력 비대칭성(Effort asymmetry). 앱 코드는 기본적인 수준이지만, 설정 파일이나 헬퍼 파일 중 하나는 복잡하거나 난독화되어 있습니다. 공격자는 앱을 복사하고 페이로드를 수작업으로 제작하며, 그 이음새가 드러납니다.
정직한 한계점들
1.5b 모델은 놓치는 부분이 있습니다. 많은 파일을 대상으로 하는 빠른 필터로는 괜찮지만, 저는 이 모델이 charCode-난독화된 드로퍼를
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기