Tool inputSchema에 참조·정규식 정책을 적용한다

매니페스트가 선언한 inputSchema는 외부가 정하는 입력이다. JSON Schema
검증기는 문서 밖 $ref를 만나면 그 주소로 직접 조회하므로 매니페스트가
서버의 outbound 호출 대상을 정하는 통로가 된다. pattern은 joni와
graal-js가 없어 java.util.regex의 백트래킹 경로로 처리되고, 인증이 없는
경계(ADR-0006)라 호출 빈도를 줄여 주는 계층도 없다.

ToolSchemaReferencePolicy가 문서 밖 참조와 미지원 dialect를 막고,
ToolSchemaPatternPolicy가 정규식 길이·무한 수량자 개수·중첩 반복을
검사하며 pattern을 쓰는 필드에 maxLength를 요구한다. 길이를 묶을 수
없는 patternProperties는 거부한다.

검사는 ToolMetadata의 표준 생성자 한 곳에서만 한다. Portal 매니페스트
파싱, local 파일 로딩, Redis snapshot 역직렬화가 모두 이 생성자를
지나므로 조회 경로가 늘어도 검사 지점은 하나로 남는다. 위반은
IllegalStateException이라 기존 매니페스트 형식 오류와 같게 다뤄지고
bundle 단위 실패 격리가 그대로 적용된다.

근거와 한계는 ADR-0011, ADR-0012에 있다. ADR-0012가 classpath 근거로
인용하는 docs/sbom도 함께 가져온다.

192개 테스트 전부 통과. 기존 169건은 새 검사에 걸리지 않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 17:20:50 +09:00
parent 58d3014a0f
commit ad7fccbed1
11 changed files with 1552 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# ADR-0011 Tool inputSchema는 문서 밖을 참조하지 않는다
- 상태: Accepted
- 결정일: 2026-08-18
- 관련 결정: [ADR-0006](ADR-0006-no-authentication-in-mcp.md) · [ADR-0004](ADR-0004-execution-guardrails.md)
## 배경
MCP Java SDK를 도입하면서 JSON Schema 2020-12 검증을 `com.networknt:json-schema-validator`에 위임했다
([mcp-java-sdk-adoption.md](../mcp-java-sdk-adoption.md)). 그런데 JSON Schema의 `$ref`는 같은 문서 안뿐 아니라
**다른 주소의 문서**를 가리킬 수 있고, 검증기는 그런 참조를 만나면 그 주소로 직접 조회를 시도한다.
`inputSchema`는 Tool Service 매니페스트에서 온다. 즉 매니페스트에 이런 schema가 실리면
```json
{"type":"object","properties":{"q":{"$ref":"http://any-host/whatever.json"}}}
```
MCP가 그 주소로 요청을 보낸다. 이것은 [AGENTS.md §2](../../AGENTS.md)의 불변식과 정면으로 어긋난다.
> outbound 주소는 설정에서만 온다. 요청 값도 매니페스트도 호출 대상을 바꾸지 못한다.
매니페스트가 선언한 `endpoint`를 무시하는 규칙은 이미 있고 테스트로 잠겨 있다. `$ref`는 같은 불변식을
같은 방식으로 깨는데 통제가 없던 경로였다. SDK 도입이 열어 놓은 구멍이다.
[ADR-0006](ADR-0006-no-authentication-in-mcp.md)에 따라 MCP는 인증·인가를 하지 않으므로, 이 경로 앞에서
호출자를 걸러 주는 계층도 없다.
## SDK 설정으로는 막을 수 없다
`DefaultJsonSchemaValidator``SchemaRegistry`를 생성자 안에서 직접 만들고 `private final`로 들고 있다.
공개 생성자는 `()``(ObjectMapper)` 둘뿐이라, 참조 해석 정책을 담은 설정을 밖에서 넣을 자리가 없다.
검증기 쪽에서 끄는 선택지는 존재하지 않는다.
## 결정
**Tool의 `inputSchema`는 문서 밖을 가리키는 참조를 담을 수 없다.** 검증기에 넘기기 전에, schema가 Registry로
들어오는 시점에 거부한다.
| 대상 | 규칙 |
|---|---|
| `$ref`, `$dynamicRef` | 값이 `#`으로 시작해야 한다. 즉 같은 문서 안의 위치만 가리킨다 |
| `$schema` | 선언했다면 `https://json-schema.org/draft/2020-12/schema`여야 한다 |
| `$id` | 제한하지 않는다 |
`$id`를 열어 두는 이유는, 문서 밖 참조가 모두 막히면 base URI가 무엇이든 조회가 일어나지 않기 때문이다.
막을 이유가 없는 것까지 막으면 정상 Tool만 거부된다.
검사 지점은 `ToolMetadata`의 표준 생성자다. Portal 매니페스트 파싱, local 파일 로딩, Redis snapshot 역직렬화가
모두 이 생성자를 지나므로 **경로마다 검사를 흩어 놓지 않아도 우회 경로가 생기지 않는다.**
위반은 기존 매니페스트 형식 오류와 같게 다룬다. 따라서 bundle 단위 실패 격리와 "Redis 실패는 언제나 cache miss"
불변식이 그대로 적용되고, 한 Tool의 잘못된 schema가 다른 bundle의 정상 Tool을 지우지 않는다.
## 검토한 대안
| 대안 | 채택하지 않은 이유 |
|---|---|
| 검증기 설정으로 원격 해석 차단 | 위 절대로 주입 지점이 없다 |
| `JsonSchemaValidator`를 직접 구현 | SDK에 표준 검증을 위임한다는 도입 전제를 되돌리게 된다. networknt API를 우리가 떠안고, SDK 업그레이드마다 정책이 조용히 어긋날 수 있다 |
| egress 방화벽만으로 차단 | 심층 방어로는 유효하지만 단독으로는 부족하다. 플랫폼 설정에 의존하고, 차단되지 않은 내부 주소에는 여전히 도달한다 |
egress 통제는 이 결정을 대체하지 않고 함께 둔다.
## 영향
- Tool Service는 `inputSchema`를 자기 문서 안에서 완결시켜야 한다. 공통 타입은 `$defs`로 같은 문서에 넣고
`#/$defs/...`로 참조한다. 이 항목은 합의 대상이 아니라 계약이므로
[extension-points.md](../extension-points.md)의 협의 목록에서 뺀다.
- `format` 키워드의 검증 강도와 허용 keyword 범위는 여전히 미확정이며 협의 목록에 남는다.
- 새 참조 keyword가 JSON Schema에 추가되면 이 결정을 함께 갱신한다. 규칙은
`ToolSchemaReferencePolicy`가 소유하고 `ToolSchemaReferencePolicyTest`가 잠근다.