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:
88
docs/decisions/ADR-0012-tool-input-schema-pattern-budget.md
Normal file
88
docs/decisions/ADR-0012-tool-input-schema-pattern-budget.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# ADR-0012 Tool inputSchema의 정규식에 예산을 둔다
|
||||
|
||||
- 상태: Accepted
|
||||
- 결정일: 2026-08-18
|
||||
- 관련 결정: [ADR-0011](ADR-0011-tool-input-schema-stays-in-document.md) · [ADR-0006](ADR-0006-no-authentication-in-mcp.md) · [ADR-0004](ADR-0004-execution-guardrails.md)
|
||||
|
||||
## 배경
|
||||
|
||||
`inputSchema`의 `pattern` 검증은 `java.util.regex`로 처리된다. `com.networknt:json-schema-validator`의
|
||||
ECMAScript 엔진은 joni나 graal-js가 있을 때만 쓰이는데 둘 다 해석하지 않으므로
|
||||
([SBOM](../sbom/README.md)), 기본 경로인 `JDKRegularExpression`이 `Pattern.compile` 후 `Matcher.find()`를
|
||||
호출한다. `matches()`가 아니라 `find()`라서 모든 시작 위치를 시도한다.
|
||||
|
||||
이 엔진은 백트래킹 기반이라 정규식과 입력의 조합에 따라 처리 시간이 폭증한다. 정규식은 Tool Service
|
||||
매니페스트에서 오고 입력은 Agent Builder에서 오며, [ADR-0006](ADR-0006-no-authentication-in-mcp.md)에 따라
|
||||
호출자를 걸러 주는 계층이 없다. 한 요청이 스레드를 붙잡으면 그대로 Tomcat 스레드 고갈로 이어진다.
|
||||
|
||||
## 측정
|
||||
|
||||
규칙을 감으로 정하지 않기 위해 JDK 21.0.11에서 직접 재어 보았다. 3초 안에 끝나지 않으면 HANG으로 적었다.
|
||||
|
||||
| 정규식 | n=100 | n=1000 | n=10000 |
|
||||
|---|---|---|---|
|
||||
| `a*a*b` (무한 수량자 2개) | 4ms | 481ms | **HANG** |
|
||||
| `a*a*a*b` (3개) | 17ms | **HANG** | **HANG** |
|
||||
| `a*a*a*a*b` (4개) | 432ms | **HANG** | **HANG** |
|
||||
| `a*a*a*a*a*b` (5개) | **HANG** | **HANG** | **HANG** |
|
||||
| `(.*,){11}P` | **HANG** | **HANG** | **HANG** |
|
||||
| `(x+x+)+y` | 5ms | **HANG** | **HANG** |
|
||||
| `^[^@ ]+@[^@ ]+$` (무한 수량자 2개) | 4ms | 4ms | **4ms** |
|
||||
| `^([A-Z]{3}-)+[0-9]+$` | 1ms | 1ms | 1ms |
|
||||
|
||||
두 가지가 드러났다.
|
||||
|
||||
**첫째, 교과서적인 중첩 수량자는 생각보다 덜 위험하고 다른 형태가 더 위험하다.** `^(a+)+$`는 n=60에서도
|
||||
0ms로 끝났다. 반면 중첩이 아닌 `a*a*a*a*a*b`는 n=100에서 이미 멈췄고, 바깥 반복이 11회로 **묶여 있는**
|
||||
`(.*,){11}P`도 멈췄다. "중첩된 무한 수량자만 막으면 된다"는 통념대로 짰다면 정작 위험한 것을 놓쳤을 것이다.
|
||||
|
||||
**둘째, 개수만으로는 가를 수 없다.** `a*a*b`와 `^[^@ ]+@[^@ ]+$`는 둘 다 무한 수량자가 2개인데 전자는
|
||||
멈추고 후자는 n=10000에서도 4ms다. 차이는 수량자가 **겹치는 문자 집합**에 걸리느냐다. `@`가 경계를 만들면
|
||||
되돌아갈 여지가 없다. 겹침 판정은 정적 분석 대상이고 일반적으로 결정 불가능하다.
|
||||
|
||||
## 결정
|
||||
|
||||
정규식 모양만으로는 안전을 가릴 수 없으므로, **가릴 수 있는 것은 모양으로 막고 나머지는 입력 길이로 묶는다.**
|
||||
검사는 `ToolSchemaPatternPolicy`가 `ToolMetadata` 생성 시점에 수행한다.
|
||||
|
||||
| 규칙 | 내용 | 근거 |
|
||||
|---|---|---|
|
||||
| 그룹 반복 | 무한 수량자를 품은 그룹을 다시 반복하면 거부. 바깥 반복 횟수에 상한이 있어도 거부 | `(x+x+)+y`, `(.*,){11}P` |
|
||||
| 수량자 개수 | 무한 수량자 4개 이상이면 거부 | 4개는 n=1000, 5개는 n=100에서 멈춤 |
|
||||
| 길이 상한 | `pattern`을 선언한 필드는 `maxLength`를 함께 선언해야 하고 256 이하여야 함 | 비용이 입력 길이를 따라 늘어남 |
|
||||
| 정규식 길이 | 512자 이하 | 분석 비용을 함께 묶음 |
|
||||
| 컴파일 | 등록 시점에 `Pattern.compile` | 잘못된 정규식이 요청 시점에 터지지 않게 |
|
||||
| `patternProperties` | 사용 금지 | 아래 참조 |
|
||||
|
||||
`maxLength` 요구가 이 결정의 핵심이다. 나머지 규칙은 겹침을 판정하지 못하므로, 실질적인 상한은 길이 제한이
|
||||
만든다. 상한이 없으면 요청 body 한도(약 1MB)까지 열린다.
|
||||
|
||||
한도 값은 설정으로 열지 않고 상수로 둔다. [ADR-0006](ADR-0006-no-authentication-in-mcp.md)의 NetworkPolicy와
|
||||
같은 이유다. values 한 줄로 사라질 수 있는 통제는 통제가 아니다.
|
||||
|
||||
## 이 결정이 하지 않는 것
|
||||
|
||||
**안전을 증명하지 않는다.** 무한 수량자 3개 이하이면서 문자 집합이 겹치는 정규식은 통과하고, `maxLength`가
|
||||
256이면 그 조합에서 수백 ms가 걸릴 수 있다. 이 결정은 위험을 없애지 않고 **측정된 폭증 구간 밖으로 옮긴다.**
|
||||
|
||||
근본적인 해결은 백트래킹하지 않는 엔진(RE2 계열)으로 바꾸거나 검증에 시간 예산을 두는 것이다. 둘 다 지금
|
||||
채택하지 않았다. 전자는 폐쇄망 반입 대상 의존성이 늘고 networknt가 그 엔진을 지원하는지 확인해야 하며,
|
||||
후자는 `java.util.regex`가 인터럽트에 반응하지 않아 검증기 내부에 우리 `CharSequence`를 넣을 수 없으면
|
||||
스레드를 버리는 방식이 된다. 필요가 생기면 이 ADR을 대체하는 새 ADR을 먼저 쓴다.
|
||||
|
||||
## 영향
|
||||
|
||||
- **Tool Service는 `pattern`을 쓰는 문자열 필드에 `maxLength`(≤256)를 함께 선언해야 한다.** 이는 매니페스트
|
||||
수용 조건의 변경이므로 Tool Service 파트와 합의가 필요하다. 현재 저장소의 schema 중 `pattern`을 쓰는 것은
|
||||
없어 기존 fixture는 영향을 받지 않는다.
|
||||
- **`patternProperties`는 쓸 수 없다.** 이 keyword는 값이 아니라 입력 객체의 **key**에 정규식을 적용하는데,
|
||||
key에는 길이를 선언할 자리가 없어 위의 `maxLength` 방식을 그대로 적용할 수 없다. `propertyNames`로 key 길이를
|
||||
묶는 방법을 검토했으나, JSON Schema는 keyword 평가 순서를 정하지 않으므로 `propertyNames`가 먼저 돈다는
|
||||
보장이 없다. 순서에 기대는 통제는 검증기 구현이 바뀌면 조용히 사라진다.
|
||||
|
||||
현재 어떤 Tool도 이 keyword를 쓰지 않으므로, 묶을 수 없는 위험을 남겨 두는 대신 쓰지 않는 기능을 닫는다.
|
||||
이는 [ADR-0006](ADR-0006-no-authentication-in-mcp.md)이 검증하지 않는 인증 코드를 지운 것과 같은 판단이다.
|
||||
동적 key가 실제로 필요해지면 key 길이를 묶는 방법을 정한 새 ADR을 먼저 쓴다. 부작용으로, `patternProperties`
|
||||
라는 이름의 업무 필드를 가진 schema도 거부된다. 실제로 나타날 가능성이 낮아 감수한다.
|
||||
- 규칙과 한도는 `ToolSchemaPatternPolicy`가 소유하고 `ToolSchemaPatternPolicyTest`가 잠근다. 위 측정을 다시
|
||||
하지 않고 한도를 바꾸지 않는다.
|
||||
Reference in New Issue
Block a user