지난주, 한 개발자는 링크드인에서 작은 크립토 스타트업 리크루터로부터 메시지를 받았습니다. 며칠간 대화를 나눈 끝에 리크루터는 리드 엔지니어 자리를 제안하며 깃허브 공개 저장소 하나를 검토해달라고 부탁했습니다. “deprecated된 Node 모듈 이슈를 확인해달라”는 구체적인 요청과 함께였습니다.

풀스택 Python 개발자 Roman Imankulov가 직접 겪은 일입니다. 평범해 보이는 채용 검토 요청이었지만 뭔가 미묘하게 이상함을 느낀 그는 곧바로 코드를 설치하지 않고, 일회용 가상서버에 저장소를 복제한 뒤 읽기 전용 모드의 AI 코딩 에이전트로 검토를 맡겼습니다. 그 결과 채용 제안 뒤에 숨어 있던 정교한 공급망 공격이 드러났습니다.
출처: A backdoor in a LinkedIn job offer – Roman Imankulov
테스트 파일로 위장한 백도어
문제의 코드는 app/test/index.js라는, 테스트 코드처럼 보이는 파일 안에 있었습니다. 약 250줄 분량으로 위장된 이 파일에는 주석으로 처리된 가짜 테스트 코드들 사이에 진짜 공격 코드가 숨어 있었습니다.
코드는 URL을 한 번에 작성하지 않고 여러 변수 조각으로 나눠뒀습니다. protocol, domain, path 같은 변수에 문자열 일부를 담아두고 나중에 조합하는 방식입니다. 이렇게 조합된 주소로 외부 서버에 접속해, 그 서버가 보내는 응답을 그대로 실행하는 코드가 핵심이었습니다. 서버가 무엇을 보내든 받는 즉시 실행하는 구조라, 공격자는 언제든 원하는 명령을 추가로 내려보낼 수 있었던 셈입니다.
이 백도어가 작동하려면 누군가 테스트를 실행해야 할 것 같지만, 실제로는 그렇지 않았습니다. app/index.js가 시작과 동시에 테스트 파일을 곧바로 불러오도록 되어 있었고, 더 결정적으로는 package.json의 prepare 스크립트가 이 진입점을 실행하도록 설정되어 있었습니다. npm은 npm install을 실행할 때 prepare 스크립트를 자동으로 함께 실행하기 때문에, 패키지를 설치하는 순간 백도어가 곧바로 작동합니다. “deprecated 모듈 이슈를 확인해달라”는 요청은 결국 피해자가 npm install을 실행하도록 유도하기 위한 미끼였습니다.
도용된 두 개의 신원
저장소의 커밋 기록은 39개, 모두 한 명의 실제 개발자 이름과 이메일로 서명되어 있었습니다. Roman이 직접 연락해보니 그 개발자는 이 저장소와 전혀 무관했고, 과거에도 깃허브에서 신원을 도용당해 저장소가 내려진 적이 있다고 답했습니다. 같은 방식의 사기 저장소를 신고해온 중이라고도 했습니다.
리크루터 역할을 맡은 링크드인 계정 역시 실제 인물의 것이었는데, 기술과는 무관한 문화 분야 저명 언론인의 프로필이었습니다. Roman이 설치가 안 된다고 말을 걸자 이 “리크루터”는 곧바로 Node.js 버전과 npm 설정에 대해 구체적으로 설명하기 시작했습니다. 평소 기술적인 내용을 다루지 않던 계정이 갑자기 npm 트러블슈팅 전문가가 된 것입니다.
AI 에이전트가 더 빨랐던 이유
Roman은 코드를 직접 읽기 전에 읽기 전용 권한만 부여한 AI 에이전트(Pi)에게 검토를 맡겼습니다. 파일 읽기, 검색, 탐색 기능만 활성화하고 실행 권한은 차단한 상태였습니다. 에이전트는 의심스러운 부분을 찾아달라는 요청을 받자 거의 즉시 문제의 테스트 파일을 지목했습니다.
이 백도어는 사람의 눈에는 그저 어설픈 초보자 코드처럼 보이도록 의도적으로 위장되어 있었습니다. 변수로 쪼개진 문자열, 주석 처리된 테스트 코드 더미 속에 묻힌 한 줄짜리 압축된 페이로드는 사람이 코드를 훑어볼 때 놓치기 쉬운 형태였습니다. 반면 AI 에이전트는 코드 전체를 빠르게 탐색하며 패턴을 대조하는 방식이라, 이런 위장에 덜 휘둘렸습니다.
이 사례가 보여주는 건 AI 에이전트가 보안 전문가를 대체한다는 이야기가 아닙니다. 의심이 든 순간 코드를 곧바로 설치하지 않고, 격리된 환경에서 권한을 제한한 도구로 먼저 들여다보는 절차 자체가 효과적이었다는 점입니다. 똑같은 코드를 사람이 눈으로 훑었다면 통과시켰을 가능성이 적지 않습니다.
Roman은 이 저장소를 깃허브에, 리크루터 계정을 링크드인에 신고했지만, 글을 쓰는 시점까지 두 계정 모두 그대로 살아있었습니다.
참고자료: npm prepare 스크립트 공식 문서

답글 남기기