Files
dap-was-dapms/docs/decisions/ADR-0012-tool-input-schema-pattern-budget.md
koseokmin d64516b50a Tool inputSchema의 문서 밖 참조와 정규식 폭증을 수신 시점에 차단한다
MCP Java SDK 도입으로 들어온 json-schema-validator는 schema의 $ref가 문서
밖을 가리키면 그 주소로 직접 조회하고, pattern 검증을 백트래킹 기반
java.util.regex로 처리한다. inputSchema는 Tool Service 매니페스트에서 오므로
매니페스트가 서버의 outbound 대상과 CPU 소비를 정할 수 있었다. AGENTS.md의
"outbound 주소는 설정에서만 온다"는 불변식이 이 경로에서 뚫려 있었다.

DefaultJsonSchemaValidator는 SchemaRegistry를 생성자 안에서 만들고 private
final로 들고 있어 정책 주입 지점이 없다. 따라서 SDK 밖에서만 막을 수 있다.

검사는 ToolMetadata의 표준 생성자에 둔다. Portal 매니페스트, local 파일,
Redis snapshot 역직렬화가 모두 이 생성자를 지나므로 우회 경로가 생기지 않는다.
위반은 기존 매니페스트 형식 오류와 같게 다뤄 bundle 단위 실패 격리와
"Redis 실패는 언제나 cache miss" 동작을 그대로 유지한다.

정규식 규칙은 JDK 21.0.11 실측으로 정했다. 통념과 달리 (a+)+는 빠르게 끝나고,
중첩이 아닌 a*a*a*a*a*b와 바깥 반복이 유한한 (.*,){11}P가 폭증했다. 겹치는
문자 집합 판정은 결정 불가능하므로 모양 검사만으로는 부족하고, pattern 필드에
maxLength 동반 선언을 요구해 입력 길이를 묶는 것이 실질적인 상한이 된다.
patternProperties는 key에 길이를 선언할 자리가 없어 사용을 금지한다.

format은 단언되지 않아 format:regex 경로가 실행되지 않는다는 사실도 계약
테스트로 고정했다. SDK 업그레이드로 단언이 켜지면 테스트가 실패한다.

Tool Service는 pattern을 쓰는 필드에 maxLength(<=256)를 선언해야 하므로
매니페스트 수용 조건이 바뀐다. Tool Service 파트와 합의가 필요하다.

근거: ADR-0011, ADR-0012

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:44:01 +09:00

6.3 KiB

ADR-0012 Tool inputSchema의 정규식에 예산을 둔다

배경

inputSchemapattern 검증은 java.util.regex로 처리된다. com.networknt:json-schema-validator의 ECMAScript 엔진은 joni나 graal-js가 있을 때만 쓰이는데 둘 다 해석하지 않으므로 (SBOM), 기본 경로인 JDKRegularExpressionPattern.compileMatcher.find()를 호출한다. matches()가 아니라 find()라서 모든 시작 위치를 시도한다.

이 엔진은 백트래킹 기반이라 정규식과 입력의 조합에 따라 처리 시간이 폭증한다. 정규식은 Tool Service 매니페스트에서 오고 입력은 Agent Builder에서 오며, ADR-0006에 따라 호출자를 걸러 주는 계층이 없다. 한 요청이 스레드를 붙잡으면 그대로 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다. 차이는 수량자가 겹치는 문자 집합에 걸리느냐다. @가 경계를 만들면 되돌아갈 여지가 없다. 겹침 판정은 정적 분석 대상이고 일반적으로 결정 불가능하다.

결정

정규식 모양만으로는 안전을 가릴 수 없으므로, 가릴 수 있는 것은 모양으로 막고 나머지는 입력 길이로 묶는다. 검사는 ToolSchemaPatternPolicyToolMetadata 생성 시점에 수행한다.

규칙 내용 근거
그룹 반복 무한 수량자를 품은 그룹을 다시 반복하면 거부. 바깥 반복 횟수에 상한이 있어도 거부 (x+x+)+y, (.*,){11}P
수량자 개수 무한 수량자 4개 이상이면 거부 4개는 n=1000, 5개는 n=100에서 멈춤
길이 상한 pattern을 선언한 필드는 maxLength를 함께 선언해야 하고 256 이하여야 함 비용이 입력 길이를 따라 늘어남
정규식 길이 512자 이하 분석 비용을 함께 묶음
컴파일 등록 시점에 Pattern.compile 잘못된 정규식이 요청 시점에 터지지 않게
patternProperties 사용 금지 아래 참조

maxLength 요구가 이 결정의 핵심이다. 나머지 규칙은 겹침을 판정하지 못하므로, 실질적인 상한은 길이 제한이 만든다. 상한이 없으면 요청 body 한도(약 1MB)까지 열린다.

한도 값은 설정으로 열지 않고 상수로 둔다. ADR-0006의 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이 검증하지 않는 인증 코드를 지운 것과 같은 판단이다. 동적 key가 실제로 필요해지면 key 길이를 묶는 방법을 정한 새 ADR을 먼저 쓴다. 부작용으로, patternProperties 라는 이름의 업무 필드를 가진 schema도 거부된다. 실제로 나타날 가능성이 낮아 감수한다.

  • 규칙과 한도는 ToolSchemaPatternPolicy가 소유하고 ToolSchemaPatternPolicyTest가 잠근다. 위 측정을 다시 하지 않고 한도를 바꾸지 않는다.