MCP 서버를 연결하면 에이전트에게 손이 생기지만, 낯선 이에게 침입 경로를 제공하기도 합니다
요약
MCP(Model Context Protocol) 서버 연결 시 에이전트가 외부 데이터에 노출되며 발생할 수 있는 보안 위협을 분석합니다. 에이전트가 도구로부터 받은 데이터를 신뢰할 수 없는 입력으로 간주하고 처리해야 함을 강조합니다.
핵심 포인트
- MCP 서버 연결은 에이전트에게 강력한 실행력을 주지만 보안 취약점을 동반함
- 에이전트가 도구의 반환값에 포함된 숨겨진 명령을 실행할 위험이 존재함
- 도구의 모든 출력값은 신뢰할 수 없는 외부 데이터(untrusted input)로 취급해야 함
- 반환된 데이터를 다음 명령의 인자로 직접 전달하지 말고 검증 과정을 거쳐야 함
MCP 서버를 연결하는 순간, 당신의 코딩 에이전트(coding agent)는 단순히 저장소(repo)를 읽고 쓰는 존재에서 벗어나 외부로 손을 뻗어 행동할 수 있는 존재가 됩니다. 데이터베이스를 읽고, API를 호출하며, 서비스에 접속하고, 웹 페이지를 가져옵니다. 이것이 바로 MCP의 모든 매력입니다. 하지만 이것은 동시에 모든 문제이기도 하며, 이 두 가지는 양면성을 가진 동일한 기능입니다.
저 또한 개인적인 플러그인 작업을 위해 도구들을 연결하는 과정을 거쳤으며, 더 큰 실수를 막아준 것은 이미 제가 겪었던 상처였습니다. 예전에 저는 모델의 출력을 페이지에 그대로 렌더링하는 AI 챗봇을 출시했다가 HTML 인젝션(HTML injection) 버그를 겪은 적이 있습니다. 그 경험을 통해 저는 아주 혹독하게 하나의 규칙을 배웠습니다. LLM이 돌려주는 모든 것은 신뢰할 수 없는 입력(untrusted input)이라는 점입니다. MCP는 그 교훈의 폭발 반경(blast radius)이 훨씬 커진 버전입니다. 왜냐하면 이제 신뢰할 수 없는 대상이 단순히 제 모델의 텍스트에 그치지 않고, 연결된 서버가 반환하기로 결정한 무엇이든 될 수 있기 때문입니다.
두 가지 서로 다른 공포, 그리고 사람들이 가진 단 하나의 공포
사람들이 에이전트 안전성(agent safety)에 대해 이야기할 때, 그들은 거의 항상 다음과 같은 상황을 의미합니다: 만약 에이전트가 파괴적인 작업을 수행하면 어떻게 될까? 파일을 삭제하거나, 강제 푸시(force-push)를 하거나, 해서는 안 될 curl 명령을 실행하는 경우 말입니다. 그 공포는 실재하며 이에 대한 실질적인 해결책도 존재합니다. 이는 나중에 다루겠습니다.
하지만 그것은 위협의 절반일 뿐이며, 눈에 보이는 절반에 불과합니다. 나머지 절반은 더 조용합니다: 만약 에이전트가 믿어서는 안 될 것을 믿게 된다면 어떻게 될까? MCP 서버가 API 응답, 데이터베이스 행(row), 이슈 본문, 이메일 스레드 등을 반환할 때, 그 반환된 콘텐츠 어딘가에 마치 명령(instruction)처럼 읽히는 한 줄이 포함될 수 있습니다. 모델은 데이터 내부에 포함되어 도착한 텍스트와 당신의 명령을 구별할 수 있는 신뢰할 수 있는 방법이 없습니다. 모델에게 이 둘은 동일한 채널입니다. 따라서 위험은 에이전트가 실행하는 명령에만 있는 것이 아니라, 도구를 통해 돌아온 콘텐츠에 의해 에이전트가 실행하도록 유도되는 명령에도 있습니다.
이것들은 두 개의 별개 문제이며, 두 개의 별개 방어책이 필요합니다. 대부분의 설정은 방어책을 하나만 설치하고 모든 것이 해결되었다고 가정합니다.
출력 측면: 모든 도구의 반환값을 양식 필드(form field)처럼 취급하라
챗봇 버그에서 제가 가져온 규칙이 하나 있습니다. MCP 서버가 무엇을 반환하든, 그것은 낯선 사람이 당신의 사이트 양식(form)에 입력한 문자열과 정확히 동일한 신뢰 범주에 속합니다. "내 도구에서 나온 데이터"가 아닙니다. 내 도구를 통해 전달되었을 뿐인 외부 데이터입니다.
이러한 관점의 전환은 그 데이터를 다루는 방식을 바꿉니다. 도구의 가공되지 않은 출력값(raw output)을 다음 명령의 인자(arguments)로 곧장 전달해서는 안 됩니다. 이스케이프(escaping) 처리 없이 화면에 렌더링해서도 안 됩니다. 또한, 반환된 문서에 숨겨진 명령문(imperative sentence)이 에이전트에 의해 조용히 지시 사항(directive)으로 취급되도록 내버려 두어서도 안 됩니다. 출력값은 검사해야 할 페이로드(payload)이지, 신뢰해야 할 값이 아닙니다. 당신이 연결한 서버에서 왔다는 사실이 그 데이터의 성격을 세탁해주지는 않습니다. 당신은 파이프를 연결했을 뿐, 그 파이프를 통해 흐르는 모든 것을 보증한 것은 아닙니다.
이것은 도구(tooling)가 당신을 위해 해줄 수 없는 절반의 영역입니다. 왜냐하면 도구는 할 수 없기 때문입니다. 어떤 샌드박스(sandbox) 플래그도 반환된 문자열이 정당한 API 결과인지 아니면 심어진 명령인지 알지 못합니다. 그 판단은 설정(setting)이 아니라, 당신이 데이터를 어떻게 연결(wire)하느냐에 달려 있습니다.
실행 측면: 매너가 아닌 벽
다른 절반인 파괴적인 명령에 대한 공포는 설정(settings)으로 해결할 수 있는 부분이 있습니다. 이는 대충 처리하기 쉽기 때문에 정확하게 구현할 가치가 있습니다.
모델에게 위험한 일을 실행하지 말라고 정중하게 요청하는 것은 매너(manners)이며, 매너는 입력값이 모델을 설득하는 순간 무력화됩니다. 당신이 원하는 것은 벽(wall)입니다. Claude Code에서는 그것이 샌드박스(sandbox)입니다. 이를 활성화하면 Bash 실행이 OS 수준에서 격리되며, 에이전트가 생성하는 모든 하위 프로세스(subprocess)가 이 제한을 상속받습니다. macOS에서는 Seatbelt, Linux와 WSL2에서는 Bubblewrap을 사용합니다.
// .claude/settings.json
{
"sandbox": {
...
제가 놀랐던 부분은 문서에 명시된 바로 이 내용입니다. 샌드박스 (sandbox)는 작업 디렉터리 (working directory)에 대한 쓰기 (writes)를 제한하지만, 기본적으로는 머신의 대부분에 대한 읽기 (reads)를 여전히 허용합니다. 즉, 별도로 설정하지 않으면 ~/.aws/credentials나 ~/.ssh/와 같은 자격 증명 (credentials)을 읽을 수 있다는 뜻입니다. 샌드박스는 쓰기와 실행 (executing)을 둘러싼 벽이지, 읽기를 둘러싼 벽이 아닙니다. 읽기는 거부 규칙 (deny rules)을 통해 스스로 차단해야 합니다:
{
"permissions": {
"deny": [
...
이것이 실제로 중요한 조합이며, 두 영역이 만나는 지점입니다. 만약 도구 (tool)의 반환 값이 에이전트 (agent)를 설득하여 비밀 정보를 유출 (exfiltrating)하도록 유도한다면, 읽기 거부 (read-deny)는 비밀 정보를 읽는 것을 막는 역할을 하고, 네트워크 명령 거부 (network-command deny)는 이를 전송하는 것을 막는 역할을 합니다. 출력 측 (output-side)의 문제는 에이전트가 시도하게 만드는 원인이 되고, 실행 측 (action-side)의 벽은 그 시도를 실패하게 만드는 역할을 합니다. 어느 하나가 다른 하나를 보완해주지 않습니다.
세 번째: 신뢰하지 않거나 필요하지 않은 것은 연결하지 마세요
서버 그 자체가 공격 표면 (attack surface)입니다. 검증하지 않은 연결된 MCP 서버는 에이전트로 통하는 채널을 가진 채 당신의 루프 (loop) 안에서 실행되는 코드입니다. 따라서 영리하진 않지만 다음 세 가지 습관을 권장합니다:
연결하기 전에 출처를 검증하세요. 누가 작성했는지, 코드를 볼 수 있는지 확인하십시오. MCP 서버는 닫고 잊어버릴 수 있는 브라우저 탭이 아닙니다. 연결되어 있는 동안에는 당신의 신뢰 경계 (trust boundary)의 일부입니다.
사용하지 않는 서버는 제거하세요. 유휴 (idle) 상태인 모든 서버는 공격 표면인 동시에 컨텍스트 무게 (context weight)가 됩니다. /mcp 명령어를 통해 실제로 연결된 것과 한때 연결했다가 잊어버린 것을 구분할 수 있습니다.
그리고 제가 가장 자주 사용하는 방법입니다. 만약 CLI가 이미 그 역할을 수행하고 있다면, 서버를 아예 구축하지 마세요. 저는 WordPress를 wp-cli로 제어하는데, 이를 MCP 서버로 감싸기보다는 필요할 때 스킬 (Skill)에서 호출합니다. 항상 연결되어 있는 표면을 하나 줄이고, 신뢰할 수 없는 콘텐츠를 반환하는 요소를 하나 줄이는 것입니다. "서버를 연결하는 것"과 "명령을 호출하는 것"은 위험의 종류가 다르며, 후자가 종종 더 작은 위험을 갖습니다.
제가 현재 실제로 확인하는 것들
어떤 MCP 서버를 연결하기 전에, 다음 세 가지 질문을 이 순서대로 던져봅니다. 첫째, 이 서버가 어디에서 왔는지 신뢰할 수 있는가? 둘째, 이 서버가 내 에이전트에게 내리도록 유도할 수 있는 최악의 명령은 무엇이며, 그 명령이 거부 규칙 (deny rules)에 의해 차단되어 있는가? 셋째, 출력값이 도달하는 모든 곳에서 신뢰할 수 없는 입력 (untrusted input)으로 취급될 것인가, 아니면 내가 낯선 이의 텍스트를 그대로 싱크 (sink)로 전달하려는 것인가?
샌드박스 (sandbox)는 이 질문 중 정확히 하나에만 답해줍니다. 나머지 두 가지는 온전히 나의 책임이며, 실제로 저를 가장 먼저 괴롭혔던 문제들이기도 합니다. 서버를 연결하는 것은 에이전트에게 진정으로 손이 생기는 순간입니다. 다만, 그 손은 도구의 반대편을 잡고 있는 누구에 의해서든 조종될 수 있다는 점을 기억할 가치가 있습니다.
저는 WordPress 플러그인을 제작하며, https://raplsworks.com/에서 AI 툴링 (tooling)과 보안에 관한 글을 씁니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기