안전한 PHP 및 JS 패키지 업데이트를 위한 Claude 코드 스킬
요약
의존성 업데이트 과정에서 발생하는 공급망 공격 위험성을 다루며, AI 에이전트가 안전하게 패키지를 관리할 수 있는 '편집증적' 규칙을 설정하는 방법을 제시합니다. 특히 `minimumReleaseAge`와 같은 격리 기간 게이트를 활용하여 오염된 릴리스의 잠재적 피해를 사전에 차단하는 것이 중요함을 강조합니다.
핵심 포인트
- AI 에이전트 시대에는 공급망 공격 위험성이 매우 높습니다.
- 패키지 업데이트 시 '최소 출시 연령(minimumReleaseAge)' 규칙을 적용해야 합니다.
- pnpm이나 Bun 같은 도구는 격리 기간 게이트 기능을 제공하여 보안성을 강화합니다.
프로젝트에서 의존성(dependency)을 업데이트하는 것이 몇 주, 아니 몇 달이 걸리는 것은 이상한 일이 아닙니다. 그리고 사람들이 마침내 업데이트할 때, 그들은 모든 것을 한 번에, 아무것도 확인하지 않고 전력을 다해 처리합니다.
이러한 습관은 항상 위험을 내포했지만, 업데이트를 수행하는 AI 에이전트의 새로운 세상에서는 매우 현실적인 위협인 공급망 공격(supply chain attacks)과 정면으로 충돌합니다.
문제점: install은 임의 코드 실행 기능입니다
우리가 모두 의존하는 패키지 생태계는 지난 몇 년 동안 이것이 얼마나 심각해질 수 있는지 정확하게 보여주었습니다. 2025년 9월, [chalk]와 debug의 일부로, 총 18개 패키지 중 하나가 한 유지 관리자의 npm 계정이 가짜 2FA 재설정 이메일을 통해 피싱되면서 암호화폐 클리퍼(crypto-clipper)를 배포하기 시작했습니다.
며칠 후, Shai-Hulud worm이 자체적으로 수백 개의 패키지를 파괴했습니다. 이 워름은 설치 후 스크립트(post-install script)를 통해 감염된 모든 장치에서 npm 토큰을 훔쳐내고, 이를 사용하여 자신과 같은 더 많은 감염된 버전을 게시하는 데 사용했습니다. 그리고 그보다 몇 주 전에는 Nx compromise가 개발자 장치에 설치 후 페이로드(post-install payload)를 심어, Claude나 Gemini 같은 로컬에 설치된 AI 코딩 CLI가 지갑과 자격 증명을 찾아내어 유출하도록 유도했습니다. 마지막 사례는 모든 에이전트 소유자가 정신을 차리게 해야 합니다. 즉, 우리 자신의 에이전트가 강탈자(burglars)로 징집되는 것입니다. 패턴은 일관적입니다. 악성 버전이 게시되어 몇 시간 또는 며칠 동안 피해를 입힌 다음, 발각되어 제거됩니다.
이를 바탕으로 저는 여러 규칙이 충족될 때까지 업데이트를 수행하지 않기로 결정했습니다. 이 규칙들을 Claude 스킬에 새겨서 제 에이전트들이 이를 처리하도록 하기로 했습니다.
AI 에이전트 스킬: 설정 파일로서의 편집증
Claude Code에서 **스킬(skill)**이란 에이전트가 특정 작업과 일치할 때 로드하는 지침을 담은 마크다운 파일일 뿐입니다. 이를 통해 제가 어렵게 얻어낸 편집증적 규칙들을 단 한 번만 인코딩하여, 절대 지치지 않고, 금요일 오후에 실수하지 않으며,
손상된 릴리스는 거의 항상 수명이 짧습니다. 오염된 chalk와 debug 버전은 npm이 제거하기까지 약 두 시간 동안 활성화되어 있었습니다. 악성 Nx 릴리스는 네 시간 동안 지속되었습니다. 악성 버전이 배포되고, 누군가 알아차리며, 제거됩니다. 릴리스 연령 게이트(release age gate)를 사용하면 오염된 기간을 완전히 건너뛸 수 있습니다. 위에 언급된 모든 사건들은 제가 경계심이 강해서가 아니라, 해당 버전들이 설치하기에 너무 어렸기 때문에 기기를 전혀 건드리지 않고 지나갔을 것입니다.
제 pnpm 프로젝트에서는 이것이 pnpm-workspace.yaml 파일의 단 한 줄로 처리됩니다 (값은 분 단위이므로 10080은 7일입니다):
minimumReleaseAge: 10080
pnpm 11부터는 기본적으로 하루 동안의 격리 기간(quarantine)까지 제공하며, Bun도 bunfig.toml에서 minimumReleaseAge와 동일한 아이디어를 가지고 있습니다. Composer에는 이에 상응하는 기능이 없기 때문에 PHP 스킬은 이를 수동으로 강제합니다: 모든 후보 버전의 릴리스 날짜를 읽고, 7일보다 어린 것은 건너뛰고
allowBuilds:
esbuild: true
sharp: true
그 외 모든 것은 설치되지만 실행되지 않습니다. 동일한 파일은 blockExoticSubdeps: true를 설정하여, 레지스트리 대신 무작위 git 저장소나 tarball URL에서 전이 의존성(transitive dependencies)이 몰래 들어오는 것을 막습니다. Bun 역시 유사한 입장을 취하며 기본적으로 임의의 의존성에 대해 라이프사이클 스크립트(lifecycle scripts)를 실행하지 않습니다.
규칙 4: 모든 것을 정확하게 고정하기 (Pin everything, exactly)
^, ~, 범위 지정자(ranges)는 사용하지 않습니다. 모든 의존성은 정확한 버전으로 고정되며, 업데이트는 광범위한 업데이트 명령을 통해서가 아니라 명시적인 composer require package:1.2.3 또는 pnpm add package@1.2.3를 통해 이루어집니다. 제 pnpm-workspace.yaml은 savePrefix: ""로 이를 뒷받침하여, pnpm이 기본적으로 ^ 범위 대신 정확한 버전을 저장하도록 만듭니다. 이는 두 가지 역할을 합니다. 첫째, 모든 버전 변경 사항을 diff에서 확인할 수 있게 하여 (다음 lockfile 재생성 시 아무것도 조용히 변하는 일이 없음), 둘째, 에이전트가 제가 승인하지 않은 것을
이 스킬은 단계를 건너뛰지 않습니다. 왜냐하면 단계 건너뛰기가 파일에 없기 때문입니다. 이제 제 업데이트는 매주 이루어지고 있으며, 공격에 대한 검사는 위임되었습니다.
여기서 skills를 확인하실 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기