Files
dap-was-dapms/docs/decisions/ADR-0012-tool-input-schema-pattern-budget.md
koseokmin ad7fccbed1 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>
2026-09-15 17:20:50 +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가 잠근다. 위 측정을 다시 하지 않고 한도를 바꾸지 않는다.