메뉴
HN
Hacker News 51일 전

코드를 실행하는 설정 파일: 공급망 보안의 맹점

IMP
9/10
핵심 요약

최근 악성코드 캠페인(마이아즈마 웜)이 VS Code, Cursor, Claude Code 등 주요 IDE와 AI 코딩 에이전트의 설정 파일을 악용해 개발자의 모르게 악성 코드를 실행하는 사례가 확인되었습니다. 설정 파일에 숨겨진 명령어는 클라우드 및 인증 토큰 등을 탈취하며, 대부분의 개발자가 코드 리뷰 없이 신뢰 프롬프트를 통과시키기 때문에 매우 위협적입니다. 개발자들은 저장소 클론이나 폴더 열기만으로도 공격에 노출될 수 있으므로 설정 파일에 대한 철저한 검토가 시급합니다.

번역된 본문

블로그로 돌아가기

코드를 실행하는 설정 파일: 공급망 보안의 맹점

맬웨어 SafeDep 팀 • 2026년 6월 6일 • 10분 분량

목차

저장소를 복제(clone)하여 에디터에서 여는 것만으로도 개발자가 단 한 줄의 코드도 읽기 전에 공격자의 코드가 실행될 수 있습니다. 여기서 작동하는 것은 악의적인 의존성(dependency)이나 숨겨진 설치 스크립트가 아닙니다. IDE, AI 코딩 에이전트 또는 패키지 관리자가 자동으로 읽고 실행하는, 저장소에 이미 존재하는 평범해 보이는 설정 파일(Config file)이 트리거입니다.

VS Code, Cursor, Claude Code, Gemini CLI, npm, Composer, Bundler 등은 모두 셸 명령을 포함할 수 있는 설정 파일을 지원합니다. 일부는 의존성을 설치하거나 테스트를 실행할 때 명령을 실행합니다. 또 다른 것들은 폴더를 열거나 에이전트 세션이 시작될 때, 대부분의 경우 개발자가 읽지도 않고 클릭하여 넘어가는 일회성 신뢰 프롬프트 이후에 실행됩니다. 명령을 실행하는 설정 파일은 단순한 메타데이터가 아닌 '실행 원시 데이터(execution primitive)'이며, 공급망 공격자들은 이를 실제 공격 수단으로 사용하기 시작했습니다. 아무도 이러한 파일을 코드 리뷰하지 않습니다.

이 글에서는 실제 소스 코드를 통해 설정 파일 주입 벡터(injection vector)를 살펴보고, 더 넓은 범주의 공격 패턴을 매핑하여 diff(변경 내역)에서 이러한 패턴을 쉽게 발견할 수 있도록 합니다. 'Miasma(마이아즈마)' 웜이 그 실제 사례입니다.

icflorescu/mantine-datatable 저장소에 대한 하나의 커밋(commit f72462d9)은 서명되지 않았으며, github-actions <[email protected]>로 작성되었습니다. 커밋 메시지는 'chore: update dependencies [skip ci]'였으며, 6개의 파일을 추가했습니다. 그중 5개는 6번째 파일인 .github/setup.js라는 단일 드로퍼(dropper, 악성코드 다운로더 및 실행기)를 실행하기 위해 존재합니다. SafeDep의 Miasma 소스-리포지토리 분석은 전체 사건, 드로퍼의 내부 구조, 그리고 영향을 받은 121개의 저장소를 문서화하고 있습니다. 이 글은 범위를 좁혀 설정 파일 자체의 공격 표면(Attack surface)에 초점을 맞춥니다.

드로퍼(The dropper) 드로퍼인 .github/setup.js는 4,348,254바이트 크기이며, try/catch 블록 안에 단 하나의 구문으로 이루어져 있습니다. 이 크기는 단순한 패딩(용량 채우기)이 아닙니다. 이 파일은 암호화된 페이로드(악성코드 본체)를 포함하고 있으며, GitHub 코드 검색 기능이 인덱싱을 중단하는 대략 384KB의 한계를 넘어서도록 설계되었습니다. 따라서 저장소가 검색에 노출될 때 작은 실행 파일들이 주로 걸리게 됩니다.

파일의 첫 부분은 다음과 같습니다: 문자 코드 배열에 대한 시저 암호(Caesar shift) 변환을 통해 eval() 함수에 데이터를 공급합니다. 이를 정적으로 디코딩(실행하지 않고 시프트 값 4를 적용)하면, 자격 증명(credential)을 탈취하는 스테이지별 Bun 로더가 AES 복호화를 통해 드러납니다. 이 탈취 악성코드는 AWS, Azure, GCP, Vault, Kubernetes, npm 및 GitHub의 비밀 키(Secrets)를 스캔한 뒤, 공격자가 만든 공개 GitHub 저장소로 이를 유출합니다. 해당 디코딩 과정은 Miasma 난독화 해제 분석 글에서 확인할 수 있습니다.

이러한 난독화 형태는 이 커밋이나 Miasma 웜에만 국한되지 않습니다. 작은 회전 함수(rotational function)로 디코딩된 숫자 배열이 eval()에 전달되고, 그 안에 암호화된 두 번째 단계를 래핑하는 구조는 SafeDep이 이 웜의 여러 파동과 관련 없는 악성 패키지 캠페인에서 계속해서 재컴파일되는 것을 발견한 전형적인 악성코드 프레임워크입니다. 빌드마다 회전량과 암호화 키가 변경되어 구조는 동일해도 파일 해시값은 변경됩니다. 내부의 페이로드는 다를 수 있지만, 호스트 시스템에 침투하는 방식은 변하지 않습니다.

7개의 설정 파일 중 어느 것도 페이로드 자체를 포함하지 않습니다. 대신 각 파일은 'node .github/setup.js'라는 동일한 문자열을 담고 있으며, 개발자의 도구가 나머지 실행을 알아서 처리하도록 방치합니다.

7개의 실행 파일(The seven launchers) 5개의 실행 파일은 위에서 언급한 mantine-datatable의 단일 커밋에서 비롯되었습니다. 추가로 Composer와 Bundler 관련 파일은 동일한 캠페인의 다른 저장소에서 발견되었으며, 이로 인해 해당 커밋의 6개 파일을 넘어 총 7개의 실행 파일로 늘어나게 되었습니다.

아래의 각 설정은 고정된 커밋 SHA를 가진 소스 코드를 기반으로 하며, 브랜치가 초기화되더라도 퍼링링크(permalink)는 ref가 아닌 blob을 직접 가리키므로 유지됩니다.

Claude Code와 Gemini CLI는 SessionStart 훅(hook)을 사용합니다. 두 에이전트는 프로젝트에서 세션이 열릴 때 셸 명령을 실행합니다. 두 파일은 바이트 단위로 완전히 동일합니다. 출처: .claude/settings.json, .gemini/settings.json. 폴더에서 에이전트를 시작하면 개발자가 프롬프트를 입력하기도 전에 드로퍼가 실행됩니다.

Cursor는 항상 적용되는 규칙(always-applied rule)을 사용합니다. Cursor에는 셸 훅이 없으므로 공격자는 에이전트에게 파일을 실행하도록 지시하는 프로젝트 규칙을 사용했습니다. 이는 저장소에 커밋된 프롬프트 인젝션(명령어 주입)의 일종입니다. 출처: .cursor/rules/setup.mdc. 이 규칙은 프로젝트 내의 모든 Cursor 대화에 로드되며, 어시스턴트에게...

원문 보기
원문 보기 (영어)
Back to Blog Config Files That Run Code: Supply Chain Security Blindspot Malware SafeDep Team • Jun 6, 2026 • 10 min read Table of Contents Cloning a repository and opening it in an editor can run an attacker’s code before a developer reads a single line. The trigger is not a malicious dependency or a hidden install script. It is an ordinary-looking config file already sitting in the repo, the kind an IDE, an AI coding agent, or a package manager reads and acts on automatically. VS Code, Cursor, Claude Code, Gemini CLI, npm, Composer, and Bundler all support config files that can carry a shell command. Some run it when dependencies install or tests run. Others run it when the folder is opened or an agent session starts, in most cases after a one-time trust prompt that developers click through without reading. A config file that runs a command is an execution primitive, not metadata, and supply chain attackers have started using it as one. Almost nobody reviews these files. This post walks the config injection vectors with real source, then maps the broader class so the pattern is easy to spot in a diff. The Miasma worm is the worked example. One commit to icflorescu/mantine-datatable , commit f72462d9 , is unsigned, authored as github-actions < [email&#160;protected] > , titled chore: update dependencies [skip ci] , and adds six files. Five of them exist to launch the sixth, a single dropper at .github/setup.js . SafeDep’s Miasma source-repo analysis documents the full incident, the dropper internals, and the 121 affected repositories. This post stays narrow and looks at the config surface itself. The dropper The dropper is .github/setup.js , 4,348,254 bytes, one statement in a try/catch . That size is not padding. It holds the encrypted payload and stays above the roughly 384 KB limit where GitHub code search stops indexing, so the small launcher files, not the dropper, are what expose the repo to a search. Its first bytes: A Caesar shift over a character-code array feeds eval . Statically decoding it (shift of 4, never run) yields a staged Bun loader that AES-decrypts a credential stealer. The stealer scans for AWS, Azure, GCP, Vault, Kubernetes, npm, and GitHub secrets, then exfiltrates them to attacker-created public GitHub repositories. That decode is in the Miasma deobfuscation writeup . This obfuscation shape is not specific to this commit, or even to Miasma. A numeric array decoded by a small rotation function and handed to eval , wrapping an encrypted second stage, is a harness SafeDep keeps seeing recompiled across separate waves of this worm and across unrelated malicious package campaigns. The rotation amount and the encryption keys change between builds, so the file hash changes while the structure stays the same. The payload inside varies. The way it reaches the host does not. None of the seven config files contains the payload. They each carry the same one string, node .github/setup.js , and let the developer’s own tools do the rest. The seven launchers Five of the launchers come from the single mantine-datatable commit above. Two more, Composer and Bundler, come from other repositories in the same campaign, which is where the count grows past the six files in that one commit. Each config below is grounded in source at a fixed commit SHA, and the permalinks survive branch resets because they pin the blob, not the ref. Claude Code and Gemini CLI use SessionStart hooks. Both agents run a shell command when a session opens in the project. The two files are byte-identical. Source: .claude/settings.json , .gemini/settings.json . Starting an agent in the folder runs the dropper before the developer types a prompt. Cursor uses an always-applied rule. Cursor has no shell hook, so the attacker used a project rule that instructs the agent to run the file. This is prompt injection committed into the repo. Source: .cursor/rules/setup.mdc . The rule loads into every Cursor conversation in the project and asks the assistant to execute the dropper as a setup step. VS Code runs a task on folder open. No agent is required. Source: .vscode/tasks.json . Opening the folder fires the task, subject to the workspace-trust prompt. npm runs a hijacked test script. The commit appended one line to scripts . Source: package.json . This one needs a deliberate action rather than a folder open. Running the project’s tests is something developers and CI do without thinking, and it detonates the dropper, locally or in a pipeline. The first five are the launchers documented in the Miasma writeup. Two more turned up across the wider campaign, and they show the attack is not limited to editors. Composer (PHP) uses a post-install-cmd hook. It appears in braune-digital/BrauneDigitalImagineBundle . Source: composer.json . Composer runs post-install-cmd on every composer install . This is install-time execution, the classic lifecycle-hook vector rather than an editor trigger. The commit also hides in a different way. It is backdated to 2017-03-08T14:19:35Z and carries a plausible message ending in [skip ci] , so it sits in dormant history rather than at the top of the log. Bundler (Ruby) runs a top-level system() call. It sits on line one of the Gemfile in mhar-andal/MyBlok . Source: Gemfile . A Gemfile is Ruby, evaluated top to bottom every time Bundler loads it. bundle install , bundle exec , or any Rails command that reads the Gemfile runs the dropper. No install of a malicious gem needed. What about the trust prompts The editor vectors are not silent bypasses, and it is worth being precise about that. VS Code opens an unfamiliar folder in Restricted Mode , where tasks do not run until the developer clicks Trust. Claude Code and Gemini CLI show a folder-trust prompt the first time a session starts in a new directory, and the planted hooks run only after it is accepted. The attack does not defeat those prompts. It relies on developers granting trust the way they dismiss a cookie banner, and on the prompt flagging that a hook exists without making its 4.3 MB target obvious. Once a folder is trusted, the hook runs on every later session with no further confirmation, and Claude Code’s SessionStart hooks run without printing anything since version 2.1.0. Two situations skip the prompt outright: pulling the malicious commit into a repo that was already trusted, and running headless ( claude -p ), which disables trust verification. The Claude Code variant is tracked as CVE-2025-59536 and CVE-2026-21852. Gemini CLI is stricter here. It re-warns when a hook’s command changes, where Claude Code currently does not. The package-manager vectors have no trust gate at all. npm test , composer install , and bundle run their hooks as a normal part of the work, which is why they belong in the same threat model even though no folder is being opened. What makes a config file dangerous The Miasma files are one instance of a wider class. A config file is dangerous when a tool reads it and acts without asking, and when its format can carry a command. Three things decide how far it goes: Trigger. What event reads the file. Folder open, agent session start, dependency install, test run, lint. Authority. What stands between the trigger and execution. A folder-trust prompt on first open, an agent deciding to follow an instruction, or nothing at all for the package-manager hooks. Grammar. Whether the format can carry a shell command or arbitrary code. JSON hook configs carry commands by design. A Gemfile is a full programming language. Score any tool on those three and the dangerous configs stand out. The files developers almost never read, because they look like editor and tooling noise, are the worst on all three. Config file Tool Trigger What gates execution .claude/settings.json Claude Code Agent session start Folder trust, then silent .gemini/settings.json Gemini CLI Agent session start Folder trust, re-warns on change .cursor/rules/*.mdc Cursor Loaded into agent context Agent chooses to run