도구 허용 목록(allowlist)이 설명(description)을 해시화할 때 놓치는 두 번째 전달 매체
요약
AI 에이전트의 도구 정의(Tool Definition)에서 보안 취약점이 발생할 수 있는 두 번째 채널로 `inputSchema`가 지적됩니다. 기존의 해시 기반 허용 목록은 주로 '도구 설명'만 검사하여, JSON Schema 내부에 포함된 속성 레벨의 `description`, `title`, `examples` 같은 산문(prose)을 놓치기 쉽습니다. 따라서 모델이 읽는 모든 도구 정의 필드를 신뢰할 수 없는 입력물로 간주하고, 이를 대체하거나 제한해야 합니다.
핵심 포인트
- 도구 설명 외에도 JSON Schema의 속성 레벨에 산문이 존재합니다.
- 해시 기반 허용 목록은 '도구 설명'만 검사하여 취약점을 놓칩니다.
- 모델에게 전달되는 모든 도구 정의 필드를 신뢰할 수 없는 입력물로 간주해야 합니다.
- 보안을 위해 스키마의 산문 정보 역시 모델에게 노출하지 않도록 대체하는 것이 좋습니다.
MCP 도구 정의 내 신뢰할 수 없는 산문(untrusted prose)의 두 번째 채널과, 실제로 드리프트 검사(drift check)가 이를 다루는지 증명하는 테스트에 대한 메모.
팀이 MCP 도구 오염(tool poisoning)을 심각하게 받아들이기 시작하면 나타나는 패턴이 있습니다. 바로 도구 설명(tool descriptions)을 고정(pin)하는 것입니다. 설명을 한 번 검토하고, 해시를 기록한 다음, 세션 사이에 설명이 변경된 도구는 거부해야 합니다. 이는 좋은 직감인데, 도구의 설명은 문서가 아니라 모델에 대한 입력물이며, 따라서 지침(instructions)이 되기 때문입니다.
하지만 도구 정의는 여러 필드를 통해 모델에 전달되며, 해시 기반 허용 목록이 보통 간과하는 필드가 바로 inputSchema입니다.
구조 (The shape)
여기에 그 경계의 간결한 버전이 있습니다. 에이전트 주변에 허용 목록을 설정하는 방법에 대한 글에서 볼 수 있는 종류입니다:
interface AdvertisedTool { // 모든 필드는 신뢰할 수 없습니다
name: string;
description?: string;
...
설명(description)은 해시화된 다음, 우리가 검토한 자체 텍스트로 _대체_됩니다. 모델은 서버의 산문을 절대 보지 못합니다. 이 부분은 맞습니다.
하지만 마지막 줄이 무엇을 전달하는지 보세요. inputSchema는 있는 그대로(verbatim) 포워딩됩니다. 그리고 inputSchema는 JSON Schema입니다.
JSON Schema가 모든 레벨에서 산문을 전송한다 (JSON Schema carries prose at every level)
JSON Schema는 단순히 타입만을 의미하지 않습니다. description 키워드는 모든 서브스키마(subschema) — 속성별로 하나씩 포함하여 — 에서 유효하며, title과 examples 역시 마찬가지입니다:
{
"type": "object",
"properties": {
...
이 문자열은 모델이 읽는 정확한 위치에 도달합니다. 이는 도구 레벨의 설명(tool-level description)과 같은 전달 매체이며, 한 단계 아래입니다 — 그리고 sha256(t.description ?? "")는 이를 절대 볼 수 없습니다.
따라서 서버는 속성 설명을 다시 작성하거나, 또는 존재하지 않았던 곳에 추가할 수 있으며, 고정된 해시는 여전히 통과합니다. 허용 목록은 _드리프트 없음(no drift)_을 보고하지만, 모델은 새로운 지침을 읽습니다. 만약 인터페이스 주석이
도구 오염(Tool poisoning)은 보통 '설명(description)'이 모델을 유도했다는 방식으로 다루어집니다. 실제 범주는 더 광범위합니다: 모델이 읽는 도구 정의 내의 모든 것이 잠재적인 지시 채널입니다. 여기에는 다음이 포함됩니다:
- 도구의
설명(description); - 각 속성(property)의
설명(description),제목(title),예제(examples); 주석(annotations)— 읽기 전용 힌트 역시 다른 형태를 가진 신뢰할 수 없는 산문일 뿐입니다;- 그리고 한 단계 더 나아가서, 인수(arguments) 자체 — 하지만 이것은 호출자(caller)의 것이므로, 이는 별개의 신뢰 문제입니다.
이 중 하나를 고정하려면 질문은 '내 해시가 일치했는가?'가 아니라 **'모델이 실제로 무엇을 읽는지, 그리고 내 검사가 그중 무엇을 다루었는지?'**입니다. 한 필드의 해시는 하나의 문만 답합니다. 정의에는 표면(surface)이 있습니다.
해결책은 설명에 이미 적용한 것과 같습니다
서버의 스키마 산문도 모델에게 보여주지 마십시오. 두 가지 형태 모두 작습니다:
대체하십시오. 인수 검증을 위한 구조적 스키마 — 유형(type), 속성(properties) 이름, 필수 여부(required), 열거형(enum) — 는 유지하되, 노출하기 전에 모든 레벨에서 설명(description) / 제목(title) / 예제(examples)를 제거하거나 덮어쓰십시오. 모델은 형태(shape)를 얻고; 당신이 산문(prose)을 공급합니다.
표준화하여 해시하십시오. 서버의 스키마 텍스트를 유지하고 싶다면, 표준화된 형식(정렬된 키)으로 만들어 해시하고, 설명에 대해 수행하는 것과 정확히 동일하게 드리프트(drift)가 발생하면 거부하십시오. 표준화가 중요합니다: 단순한 재직렬화는 키 순서와 공백을 변경하므로, 바이트 단위로 동일한 스키마라도 다르게 해시될 수 있고 오탐지(false-positive)를 유발할 수 있습니다.
어느 쪽이든, 드리프트 검사는 이제 주장했던 운반체(carrier)까지 포괄합니다.
테스트는 사례가 아니라 제어 팔입니다
여기서 가장 유용한 테스트는 1분 안에 작성할 수 있을 만큼 작으며, 오늘날 실패하는 바로 그 테스트입니다:
- 바이트 단위로 동일한 최상위
설명(description)을 가지고 있지만, 단 하나의 속성(property)의설명(description)에서만 차이가 나는 두 개의 광고된 도구.
이를 제어 쌍(control pair)으로 작성하세요: 현재 코드로, 드리프트된 도구는 통과합니다 (그리고 거부되어야 합니다); 수정 후에는 거부되지만 변경되지 않은 도구는 여전히 통과합니다. 만약 '변경된 도구 설명이 거부되는지'만 테스트한다면, 당신은 검사를 작성한 필드를 테스트했을 뿐 — 전달 매체(carrier)를 테스트하지 않은 것입니다.
그 형태는 일반화됩니다. 시스템이 하나의 사물에 대해 여러 표현을 소비하고 그중 하나를 고정할 때, 당신의 가드(guard)가 실제로 보호 장치인지 아니면 단순한 묵인(nod)인지를 알려주는 테스트는 고정된 필드에서는 보이지 않고 오직 고정하지 않은 곳에서만 보이는 변경이어야 합니다.
전이 가능한 비트 (The transferable bit)
어떤 정의가 '모델이 읽는 사물'일 때, 그것은 필드(field)가 아니라 표면(surface)을 가집니다. 고정했다고 결정하기 전에 그 표면 — 모델이 볼 수 있는 모든 문자열 — 을 열거하세요. 그런 다음 고정된 필드를 유지하면서 그 표면의 한 요소를 이동시키는 테스트를 선택하세요. 그것이야말로 당신이 실제로 던진 질문에 답하는 유일한 테스트입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기