MCP의 실제 공격 표면은 프롬프트 인젝션이 아니라 신뢰 경계입니다 (21가지 패턴, 5가지 언어)
요약
MCP(Model Context Protocol)의 보안 위협이 프롬프트 인젝션을 넘어 신뢰 경계에서의 도구 호출로 확장됨을 경고합니다. LLM이 도구 호출의 전달 메커니즘이 되어 기존의 고전적인 앱 보안 취약점을 실행할 수 있는 위험성을 분석합니다.
핵심 포인트
- MCP 보안의 핵심은 프롬프트 인젝션이 아닌 신뢰 경계(Trust Boundary) 관리임
- LLM은 취약점을 실행하는 원격 코드 실행(RCE)의 전달 메커니즘 역할을 수행함
- MCP 서버의 취약점은 대부분 기존의 고전적인 앱 보안(Appsec) 문제와 동일함
- Python, Java, Go 등 5개 언어에서 21가지 취약점 패턴이 발견됨
우리는 프롬프트 인젝션 (Prompt Injection)이 마치 최종 단계인 것처럼 계속 이야기합니다. 하지만 그렇지 않습니다. 프롬프트 인젝션은 첫 번째 단계일 뿐입니다. 실제 피해는 그보다 한 단계 뒤, 대부분의 MCP 위협 모델 (Threat Model)이 거의 언급하지 않는 곳에서 발생합니다. 바로 신뢰 경계 (Trust Boundary) — 주입된 명령이 당신의 머신 권한으로 실행되는 실제 도구 호출 (Tool Call)로 변하는 순간입니다.
저는 몇 가지 MCP 서버를 로컬에서 구축하고 실행하고 있는데, 서버 소스 코드를 더 많이 읽을수록 우리가 잘못된 계층을 감사하고 있다는 확신이 들었습니다. 그래서 저는 다른 MCP 서버를 감사하는 것만을 목적으로 하는 작은 MCP 서버를 구축했습니다. 이것은 그 서버가 무엇을 찾는지와 그 이유에 대한 보고서입니다. 당신이 제 코드를 실행하든 안 하든, 위협 모델 (Threat Model)은 재사용 가능한 부분입니다.
당신을 밤잠 설치게 할 연쇄 반응
MCP 서버는 설계상 LLM이 호출하도록 허용된 코드입니다. 그것이 핵심입니다. 즉, 위험한 연쇄 반응은 다음과 같습니다:
신뢰할 수 없는 입력 (Untrusted Input) → 모델이 설득됨 → 도구 호출 (Tool Call) 발생 → 서버 측 코드가 당신의 장치에서 실행됨
인젝션 (Injection)은 결코 취약점이 아니었습니다. 인젝션은 단지 *전달 메커니즘 (Delivery Mechanism)*일 뿐입니다. 취약점은 도구가 호출된 후 수행하는 작업에 있습니다. 만약 서버 측 핸들러 (Handler)가 받은 인자 (Arguments)를 가지고 안전하지 않은 작업을 수행한다면, 모델은 방금 원격 코드 실행 (Remote-Code-Execution, RCE) 배달원이 된 것입니다. 그리고 모델은 설계된 대로 정확하게 동작하면서 그 일을 수행했습니다.
이는 문제를 유용한 방식으로 재정의합니다: 대부분의 MCP 보안은 단지 모델을 통해 새롭게 도달 가능해진 앱 보안 (Appsec)일 뿐입니다. 중요한 버그들은 20년 된 고전적인 것들입니다. 변한 것은 그것에 도달할 수 있는 주체입니다. MCP 이전에는 이 코드의 상당 부분이 신뢰할 수 있는 호출자에 의해서만 호출될 수 있었습니다. 이제는 당신의 에이전트 (Agent)가 읽는 웹페이지의 문장 하나가 그 코드에 도달할 수 있습니다.
반복적으로 나타나는 패턴들
MCP 서버 소스를 읽다 보면, 동일한 몇 가지 실수들이 반복해서 나타납니다. 이것들은 OWASP의 주요 사례들이며, 도구 호출 (Tool Call) 한 단계 거리에 있을 때 얼마나 심각하게 타격을 주는지에 따라 점수를 매겼습니다:
도구 인자(tool arguments)에서의 eval() / exec() / os.system() CRITICAL 직접적인 코드 인젝션 (direct code injection)
subprocess(..., shell=True), Runtime.exec(concat) HIGH 커맨드 인젝션 (command injection)
pickle.load / torch.load / ObjectInputStream HIGH 역직렬화 RCE (deserialization RCE)
...
이 중 어느 것도 생소한 것이 아닙니다. 그것이 핵심입니다. 제가 구축한 스캐너는 Python, Java, Go, C++, Rust에 걸쳐 21가지 취약점 패턴을 탐지하며, 이 중 거의 모든 패턴은 LLM(대규모 언어 모델)이 등장하기 훨씬 전부터 존재했습니다. 새로움은 버그의 종류가 아니라, 도달 가능성 (reachability)에 있습니다.
가장 중요했던 설계 결정: 목적 인지형 심각도 (purpose-aware severity)
이와 같은 스캐너의 초보적인 버전은 그저 화려하게 포장된 grep eval에 불과합니다. 이는 과도한 오탐 (false positives)을 양산하며, 사용자는 이틀째 되는 날 출력 결과를 읽기를 포기하게 만듭니다. 샌드박스화된 테스트 하네스 (test harness) 내부의 eval()은 도구의 입력 인자 (input argument)에 있는 eval()과 동일한 발견 사항이 아닙니다. 이 둘을 동일한 점수로 매기는 스캐너는 노이즈일 뿐입니다.
따라서 이 도구의 핵심은 **목적 인지형 점수 산정 (purpose-aware scoring)**입니다. 즉, 패턴이 어디에 위치하는지, 그리고 무엇이 그 패턴에 도달하는지에 따라 가중치를 부여합니다. 모델 입력에 절대 닿지 않는 CI 헬퍼 (CI helper) 내의 subprocess(shell=True)는 낮은 우선순위의 참고 사항입니다. 하지만 동일한 호출이 도구 인자에 연결되어 있다면 이는 CRITICAL (심각) 단계입니다. 이를 정확히 구분하는 것이 누군가가 실제로 조치를 취하는 보고서와 누군가가 그냥 닫아버리는 보고서 사이의 차이를 만듭니다.
이 스캐너는 그 자체로 세 가지 도구를 갖춘 MCP 서버로 배포됩니다:
audit_repo— GitHub URL을 지정하면 점수가 매겨진 보고서를 받습니다.audit_code— 코드 스니펫 (snippet)을 붙여넣으면 인라인 (inline)으로 결과 내용을 확인합니다.list_patterns— 모든 패턴과 그 심각도를 확인합니다.
이 스캐너는 Claude Desktop에서 MCP 서버로서 로컬에서 실행되기 때문에, 사용자는 에이전트가 리포지토리를 연결하기 전에 먼저 감사할 수 있습니다. 이는 곧 신뢰하려는 서버가 인자에 대해 셸 실행 (shell out)을 수행한다는 사실을 알아내기에 가장 적절한 시점입니다.
pip install mcp-security-audit
MCP를 넘어 일반화할 수 있는 두 가지 교훈
1. 인젝션(Injection)이 취약점이 아니라, 핸들러(Handler)가 취약점입니다. 방어 예산을 모델에 도달하는 입력을 필터링하는 데만 쓰지 말고, 도구 코드가 인자(Arguments)를 가지고 무엇을 하는지에 집중하여 할당하십시오. 입력 필터링은 체(Sieve)와 같지만, 안전한 핸들러는 벽(Wall)과 같습니다. 당신에게 필요한 것은 벽입니다.
2. 맥락(Context) 없는 심각도는 사람들을 당신을 무시하도록 훈련시키는 소음일 뿐입니다. 샌드박스화된 eval과 도달 가능한 eval을 구분하지 못하는 스캐너는 차단될 것입니다. 맥락 인식(Context-awareness)은 보안 도구의 있으면 좋은 기능(Nice-to-have)이 아닙니다. 그것은 누군가가 두 번째 보고서를 읽을지 여부를 결정하는 핵심 기능입니다.
솔직한 부분: 여전히 부족한 점
정적 패턴(Static patterns)은 형태상 도달 가능한 것(Reachable-by-shape)은 잡아내지만, 실제로 도달 가능한 것(Reachable-in-fact)은 잡아내지 못합니다. 인자가 실제로 위험한 싱크(Sink)로 흐른다는 것을 증명하는 오염 분석(Taint analysis)이 있다면 오탐(False positives)을 더욱 줄일 수 있을 것입니다. 이것이 이 설계의 미개척 영역이며, 아직 구축되지 않았습니다. 그리고 제가 충분히 다루었다고 확신하기 어려운 두 가지 패턴 군은 파일 제공 도구에서의 **경로 탐색 (Path traversal)**과, 외부 요청을 보내도록 설계된 fetch 방식 도구에서의 **SSRF (Server-Side Request Forgery)**입니다. 후자의 경우
- 경계(Boundary)는 어디에 위치해야 하는가? 기본적으로 모든 도구 호출(tool call)을 샌드박스(Sandbox)화할 것인가(성능 및 개발자 경험(DX) 비용 발생), 아니면 설치 시점에 스캔하고 신뢰할 것인가? 나는 설치 시점 스캔과 런타임 허용 목록(allow-lists) 방식을 선호하며, 만약 다른 의견이 있다면 설득해 보길 바란다.
- 내가 놓치고 있는 패턴은 무엇인가? 나는 21가지를 찾아냈다. 22번째 패턴을 알려달라.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기