본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 17. 04:10

capgate를 Damn Vulnerable MCP에 적용해 본 결과: 잡아낸 것과 놓친 것

요약

MCP(Model Context Protocol) 서버의 보안을 강화하는 컴파일 타임 도구인 capgate를 Damn Vulnerable MCP 환경에 적용하여 테스트한 결과입니다. capgate가 선언된 매니페스트를 기반으로 샌드박스 정책을 생성하여 프롬프트 인젝션 및 권한 오남용 공격을 효과적으로 차단함을 보여줍니다.

핵심 포인트

  • capgate는 매니페스트를 기반으로 Docker나 bwrap 등의 샌드박스 정책을 생성함
  • 실행 시점이 아닌 컴파일 타임에 보안 경계를 설정하여 공격 표면을 최소화함
  • 테스트 결과, 파일 시스템 접근 제한을 통해 민감한 데이터 탈취 공격을 성공적으로 방어함
  • 도구가 주장하는 최소한의 권한만을 허용함으로써 보안성을 확보함

능력 컴파일러(capability-compiler)가 의도적으로 취약하게 만든 10개의 MCP 서버와 만났습니다. 솔직한 점수는 이렇습니다. 한 클래스는 깔끔하게 막았고, 여러 개의 공격 범위는 줄였으며, 또 다른 것은 무용지물입니다. 어떤 것이 어느 것인지 아는 것이 핵심입니다.

공개합니다: 저는 본 게시물에서 테스트하는 Apache-2.0 샌드박스 컴파일러인 capgate의 저자입니다. DVMCP 프로젝트와 언급된 다른 도구들은 제가 만든 것이 아닙니다. 매니페스트(manifest)와 컴파일된 출력물은 repo에서 재현할 수 있습니다.

설정 (The setup)

Damn Vulnerable MCP (DVMCP)는 교육용 프로젝트입니다: 10개의 MCP 서버가 있으며, 각 서버는 프롬프트 인젝션(prompt injection), 도구 오염(tool poisoning), 과도한 권한 범위(excessive permission scope), 토큰 탈취(token theft), 명령어 인젝션(command injection) 등 하나의 공격을 시연하도록 구축되었습니다. 이는 생태계가 공유하는 적대적 테스트 환경과 가장 유사합니다.

capgate컴파일 시간 도구입니다. 사용자는 MCP 서버가 무엇을 할 수 있도록 _허용되는지_를 선언하는 매니페스트(fs:read:/workspace/**, net:connect:api.github.com:443 등, 그 외에는 아무것도 아님)를 작성하고, 이것이 구체적인 샌드박스 정책(docker run 플래그, bwrap argv 또는 egress-proxy 설정)으로 컴파일됩니다. 이는 어떤 것도 실행하거나, 트래픽을 감시하거나, 서버의 코드를 검사하지 않습니다. 선언된 기능 집합을 강제되는 경계로 바꿉니다.

따라서 이것은 공정하고 반증 가능한 테스트입니다: 각 DVMCP 과제에 대해 저는 가장 최소한의 정직한 매니페스트를 작성하고, 이를 컴파일하여 하나의 질문을 던졌습니다. capgate가 생성하는 경계가 실제로 공격을 막아내는가?

답은 전반적으로

바로 옆의 private 디렉토리에는 employee_salaries.txt, acquisition_plans.txt, 그리고 system_credentials.txt(실제 DB 비밀번호 및 클라우드 API 키)가 들어 있습니다. 프롬프트 인젝션 (Prompt-injection)된 에이전트는 그저 read_file("/tmp/dvmcp_challenge3/private/system_credentials.txt")를 호출하여 모든 것을 가지고 유유히 빠져나갑니다.

정직한 매니페스트(manifest) — 도구가 필요하다고 주장하는 내용:

{ "name": "read_file", "capabilities": ["fs:read:/tmp/dvmcp_challenge3/public/**"] }

capgate는 이를 (--target docker 옵션으로) 다음과 같이 컴파일합니다:

--rm --cap-drop ALL --security-opt no-new-privileges --read-only
--network none
--volume /tmp/dvmcp_challenge3/public:/tmp/dvmcp_challenge3/public:ro

이제 공격은 실패합니다. 이는 경로 체크 (path check)가 개선되었기 때문이 아니라, private 디렉토리가 컨테이너 내부에 마운트(mount)되지 않았기 때문입니다. read_file("/tmp/.../private/system_credentials.txt")는 _file not found_를 반환하는데, 샌드박스 (sandbox) 내부에는 해당 파일이 존재하지 않기 때문입니다. 경로 탐색 (path-traversal) 버그는 여전히 코드에 남아 있지만, capgate가 해당 경로에 도달할 수 없게 만든 것입니다. 네트워크는 꺼져 있고, 파일 시스템은 읽기 전용 (read-only)이며, 모든 권한 (capability)은 제거되었습니다.

capgate는 여기서 수행한 한 가지 근사치(approximation)에 대해 명확히 밝힙니다. 출력 결과에는 다음과 같은 notes[] 항목이 포함됩니다: "fs: ext{/tmp/dvmcp_challenge3/public/_volume mount /tmp/dvmcp_challenge3/public`로 낮아졌습니다 — Docker는 디렉토리를 마운트하며, 글로브 (glob) 패턴은 마운트할 수 없습니다. 세밀한 글로브 강제 적용은 서버의 역할입니다."

선언된 권한은 글로브 패턴이었으나, Docker는 디렉토리만 마운트할 수 있습니다. capgate는 _디렉토리_에 대한 권한을 부여하고, 출력 결과를 통해 더 세밀한 글로브 적용은 이제 샌드박스가 아닌 서버의 책임임을 알려줍니다. 이것이 이번 실습 전체의 패턴입니다. 경계는 실재하며, 그 경계가 선언보다 더 거칠게(coarser) 적용된 부분은 숨겨지는 것이 아니라 기록됩니다.

이것이 바로 capgate가 정조준한 지점입니다. 취약점의 본질은 과도하게 넓은 접근 권한이며, 권한 경계 (capability boundary)는 정확히 올바른 형태의 해답입니다. 열 개 중 하나일 뿐이지만, 깔끔한 해결책입니다.

방어하는 것이 아니라 포함하는 것: 챌린지 7, 8, 9

이것들이 정직한 중간 지점입니다. capgate는 버그를 멈추는 것이 아니라, 버그가 달성할 수 있는 범위를 축소합니다.

챌린지 7 — 토큰 탈취 (Token Theft) → 송출(egress) 단계에서 차단

해당 도구는 에러 문자열(LLM 컨텍스트로 바로 흘러 들어가는)에 베어러 토큰(bearer token)과 API 키를 유출합니다:

Authorization: Bearer {email_token.get('access_token')}
API Key: {email_token.get('api_key')}

capgate는 도구가 자신의 토큰을 읽는(reading) 행위 자체를 막을 수는 없습니다. capgate가 할 수 있는 일은 그 토큰이 _어디로 갈 수 있는지(go)_를 제한하는 것입니다. 정직한 매니페스트(manifest)는 하나의 송출 엔드포인트(egress endpoint)를 선언하며, --target egress --egress-target squid 출력 결과는 다음과 같습니다:

# capgate-egress.squid.conf (generated — do not edit)
acl to_private dst 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8 169.254.0.0/16 ::1/128 fc00::/7 fe80::/10
http_access deny to_private
...

토큰을 attacker.example.com으로 POST 하려는 오염된 도구는 프록시(proxy) 단계에서 거부됩니다. 허용 목록(allowlist)에는 정확히 하나의 호스트만 포함되어 있으며, 설정은 무조건적인 deny all로 끝납니다. 전형적인 프롬프트 인젝션에서 송출(exfiltration)로 이어지는 체인이 네트워크 경계에서 끊어집니다.

솔직한 주의사항을 명확히 밝히자면: 토큰은 여전히 모델의 컨텍스트에 도달하며, 만약 공격자가 단 하나의 허용된 채널(api.emailpro.com 자체로 보내는 정교하게 조작된 요청)을 통해 이를 빼낼 수 있다면 capgate는 이를 감지하지 못합니다. capgate는 광범위한 송출 경로를 차단하는 것이지, 상상 가능한 모든 경로를 차단하는 것이 아닙니다. (두 번째 솔직한 참고 사항: DVMCP는 이러한 토큰들을 누구나 읽을 수 있는 파일에 저장합니다. 충실한 capgate 매니페스트라면 해당 파일에 대한 fs 접근 권한을 절대 부여하지 않을 것이므로, 도구는 아예 토큰을 읽을 수조차 없을 것입니다. 송출 허용 목록(egress allowlist)은 비밀 정보가 프로세스 내에 정당하게 존재할 때를 대비한 최후의 보루입니다.)

챌린지 8 — 악성 코드 실행 (Malicious Code Execution) → 차단되지 않고 격리됨

이 사례는 문법(grammar)의 실제 한계를 드러내며, 이에 대해 분명히 짚고 넘어갈 가치가 있습니다. 해당 도구는 다음과 같습니다:

@mcp.tool()
def execute_shell_command(command: str) -> str:
    result = subprocess.check_output(command, shell=True, ...)   # 임의의 쉘 (arbitrary shell)

capgate의 capability grammar(기능 문법)는 "임의의 쉘 실행(run arbitrary shell)"을 표현할 수 없습니다. exec는 설계상 basename만 허용합니다 (exec:spawn:git). 즉, exec:spawn:*와 같은 형식은 존재하지 않습니다. 따라서 이 도구가 실제로 수행하는 작업을 정직하게 부여하는 manifest(매니페스트)를 작성하는 것은 불가능합니다. capgate의 자체 문서에서도 다음과 같이 명시하고 있습니다: "과소 선언된(under-declares) manifest는 manifest의 버그입니다." capgate는 쉘 실행(shell-exec) 도구를 안전하게 만들어주지 않으며, 그렇게 하는 척하지도 않습니다.

대신 capgate가 하는 일은 주변 서버의 폭발 반경(blast radius)을 제한하는 것입니다. 정당한(legitimate) 도구들(get_system_info, analyze_log_file)을 컴파일하면 다음과 같은 결과가 나옵니다:

--rm --cap-drop ALL --security-opt no-new-privileges --read-only
--network none
--volume /tmp/dvmcp_challenge8/logs:/tmp/dvmcp_challenge8/logs:ro

만약 execute_shell_command가 어쨌든 배포되어 실행된다 하더라도, 그것은 해당 박스 안에서 실행됩니다: 네트워크 없음, Linux capabilities(리눅스 기능) 없음, 읽기 전용 rootfs(루트 파일 시스템), 주입된 secret(비밀 정보) 없음, 오직 로그 디렉토리만 보임. 네트워크에 도달할 수 없고, 권한을 상승(escalate)할 수 없으며, 자격 증명(credential)을 볼 수 없는 성공적인 RCE(원격 코드 실행)는 극적으로 규모가 작은 사고가 됩니다. 이것이 바로 심층 방어(defense-in-depth)이며, 명시적으로 말하자면 예방(prevention)이 아닙니다.

Challenge 9 — Command Injection (커맨드 인젝션) → 사설 범위는 차단되지만, 공용 외부 유출은 차단할 수 없음

network_diagnostic(target, options)는 사용자 입력을 shell=True로 직접 파이프라인 연결합니다. 이것은 네트워크 도구이므로, 정직한 manifest는 net:connect:*를 부여해야 합니다. 그리고 capgate는 그에 따른 비용을 정직하게 보여줍니다:

{ "egress": [{ "host": "*", "port": null, "blockPrivate": true }] }

와일드카드 host는 egress(외부 유출) _allowlist(허용 목록)_가 도움이 될 수 없음을 의미합니다. "모든 곳"을 allowlist에 추가할 수는 없기 때문입니다. 하지만 blockPrivate가 자동으로 설정되며, nftables 타겟이 커널 수준에서 이를 강제합니다:

table inet capgate {
  chain egress {
    type filter hook output priority 0; policy drop;
...

따라서 커맨드 인젝션 (command injection)은 여전히 실행되며 공용 인터넷 (public internet)에 도달할 수 있지만, 169.254.169.254 (클라우드 메타데이터), 127.0.0.1 (로컬 서비스), 또는 RFC1918 내부 호스트로 피벗 (pivot)할 수는 없습니다. 그리고 capgate는 나머지 부분을 허위로 생성하기를 거부합니다. 와일드카드 규칙은 "nftables는 호스트네임이 아닌 IP를 필터링합니다; ''는 IP 허용 목록으로 표현될 수 없습니다. 와일드카드/호스트네임 규칙에는 'squid' 타겟을 사용하십시오."_라는 이유와 함께 unenforceable[] 필드에 나타납니다. capgate는 단순히 조용히 차단하는 대신, 자신이 할 수 없는 것과 대신 무엇을 해야 하는지를 알려줍니다.

솔직한 실패: Challenge 1 — 기본 프롬프트 인젝션 (Basic Prompt Injection)

Challenge 1 도구는 아무런 힘이 없습니다. 이는 메모리 내 딕셔너리 (in-memory dictionary)를 읽기 때문입니다:

@mcp.tool()
def get_user_info(username: str) -> str:
    users = {"admin": "System administrator with full access", ...}
...

공격의 핵심은 도구가 무엇에 _도달하는가_가 아닙니다. 주입된 텍스트를 통해 모델이 자신의 지침을 무시하도록 설득하는 것입니다. 정직한 매니페스트 (manifest)는 비어 있으며 ("capabilities": []), capgate는 이를 생성할 수 있는 가장 폐쇄적인 샌드박스 (sandbox)로 컴파일합니다:

--rm --cap-drop ALL --security-opt no-new-privileges --read-only --tmpfs /tmp --network none

그리고 프롬프트 인젝션은 여전히 완벽하게 작동합니다. capgate는 도구가 무엇을 _할 수 있는지_를 제한할 뿐, LLM이 (속아서) 그것을 _하도록 유도될 수 있는지_에 대해서는 아무런 말이 없습니다. Challenge 1, 2 (도구 포이즈닝, tool poisoning), 그리고 6 (간접 인젝션, indirect injection)은 모두 모델 계층 (model layer)에 존재하며, 권한 컴파일러 (capability compiler)는 이 세 가지 모두에 대해 잘못된 도구입니다. 이러한 공격이 무언가에 도달하려고 시도할 경우 폭발 반경 (blast radius)을 줄여줄 수는 있지만, 조작 자체를 방지하지는 못합니다.

샌드박스 컴파일러가 프롬프트 인젝션을 막아준다고 말하는 사람은 무언가를 팔아먹으려는 사람입니다. 그렇지 않습니다. capgate는 탈취된 도구가 건드릴 수 있는 범위를 제한함으로써 프롬프트 인젝션을 덜 유용하게 만들 뿐입니다.

스코어카드 (The scorecard)

#Challengecapgate의 효과
1Basic Prompt Injection❌ 방지 불가 (모델 계층) — 피해 범위(blast radius)만 제한
...
1개의 깔끔한 방지. 4개의 의미 있는 봉쇄. 3개의 솔직한 실패. 2개의 범위 외 사례.

이것이 실제 적대적 코퍼스(adversarial corpus)에 맞선 기능 컴파일러(capability compiler)의 실제 모습입니다. 이것은 만능 해결책(silver bullet)이 아니며, capgate가 손댈 수 없는 사례들은 정확히 나머지 MCP 보안 스택(스캐너, 런타임 모니터, 모델 자체 방어 기제)이 보완하기 위해 존재하는 사례들입니다. capgate는 하나의 계층입니다. capgate는

node dist/cli.js compile examples/dvmcp/challenge3-excessive-permission.json --target docker --pretty
node dist/cli.js compile examples/dvmcp/challenge7-token-theft.json --target egress --egress-target squid --pretty
node dist/cli.js compile examples/dvmcp/challenge9-command-injection.json --target egress --egress-target nftables --pretty

만약 여러분이 오늘날 MCP 서버를 실행하면서 devcontainer를 설정하거나 마운트 목록(mount list)을 지정하는 방식으로 직접 권한 경계(capability boundary)를 결정하고 있다면, 저는 그 결정이 여러분에게 어디에 근거하여 이루어지는지, 그리고 그 비용이 얼마나 드는지 진심으로 알고 싶습니다. 그것이 바로 이 모든 시도가 다루고 있는 실제 열린 질문(open question)입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0