Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/security-policy-enforcement/case/case-a06-f011-mongoregexpolicy-forbidden.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

11 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn assets evidence source
CASE a06-f011-mongoregexpolicy-forbidden MongoRegexPolicy.forbidden()은 금지하지 않는다 security-policy-enforcement clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a06-f011-mongoregexpolicy-forbidden 2026-09-02
key file
a06-f011-mongoregexpolicy-forbidden-probe ../../../final/evidence/rendered/a06-f011-mongoregexpolicy-forbidden-probe.svg
key file
a06-f011-mongoregexpolicy-forbidden ../../../final/evidence/rendered/a06-f011-mongoregexpolicy-forbidden.svg
../../../final/evidence/raw/a06-f011-mongoregexpolicy-forbidden-probe.txt
../../../final/evidence/raw/a06-f011-mongoregexpolicy-forbidden.txt
원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L740 이다. 등급은 P3 이다. 금지 팩토리가 길이 1 정책이라는 관찰, 앵커 문자가 네 검사를 통과한다는 판정, 두 도우미만 막힌다는 대비, 그리고 필드가 정규식 연산자를 등록해야 도달한다는 조건이 그 절에 있다.
그 절은 두 도우미의 이스케이프 결과가 항상 다섯 자 이상이라고 적는데, 포함 쪽 최단은 네 자다. 결론은 같다.
통과하는 조합이 정확히 하나라는 것, 최대 길이 0 이 생성자에서 막힌다는 것, 매치되는 것이 그 필드의 문자열 값이라는 것, 그리고 이 팩토리의 호출 지점이 없다는 것은 이 기록에서 확인했다.

MongoRegexPolicy.forbidden()은 금지하지 않는다

금지가 별도 상태가 아니라 최대 길이 1 로 표현되어 있다. 그 정책을 통과하는 패턴과 플래그의 조합은 하나뿐이고, 그 하나는 대상 필드에 든 문자열 값을 전부 매치한다.

관계

  • 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다 금지를 별도 상태로 두어야 하는 이유다.
  • sanitize가 아니라 reject가 기본이다 정화가 아니라 거절이 기본이어야 한다는 규칙이다.
  • 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다 두 사례 모두 검사 방식이 막으려는 대상과 맞지 않는다.

문제

정규식 정책은 최대 길이와 허용 플래그와 앵커 요구 셋으로 이루어진 레코드다. 금지를 뜻하는 자리는 그 셋에 없다.

컴팩트 생성자는 최대 길이가 0 이하면 거부한다. 음수 길이는 관대한 정책이 아니라 오타라는 판단이고 그 판단은 옳다. 다만 그 때문에 정규식 없음을 뜻할 수 있는 값이 표현 불가능해지고, 금지 팩토리는 표현 가능한 가장 작은 값을 쓴다.

결론

길이 1 이하이면서 앵커로 시작하는 문자열은 앵커 문자 하나뿐이다. 허용 플래그 집합이 비어 있으므로 플래그도 빈 문자열이어야 한다. 통과하는 조합은 그 둘의 짝 하나다.

그 짝은 대상 필드에 문자열이 든 문서를 전부 매치한다. 값이 문자열이 아니거나 필드가 없으면 매치되지 않는다. 무력화되는 것은 그 컬렉션에서 정규식 검색의 대상이 되는 값 전부다.

이스케이프 도우미 둘은 어떤 입력으로도 이 정책을 통과할 수 없다. 입력이 길수록 출력도 길어지는데, 가장 짧은 것이 다섯 자와 네 자다. 금지 정책이 실제로 막는 것은 호출자가 문법을 기여할 수 없는 두 경로이고, 남기는 것은 문법을 그대로 받는 경로의 그 한 짝이다.

도달에는 선택 둘이 겹쳐야 한다. 이 팩토리를 부르는 것과, 어떤 필드가 정규식 연산자를 명시로 등록하는 것이다. 기본 연산자 집합에 정규식은 없다. 그리고 지금 저장소에는 앞의 선택이 없다.

이 파일의 javadoc 은 대체로 자기 한계를 함께 적는다. 중첩 수량자 검사는 안전 증명이 아니라 필터라고 스스로 밝힌다. 조건 없이 단언하는 문장은 금지 팩토리 위의 한 줄뿐이고, 그 한 줄이 참이 아니다.

수정은 정책에 명시적인 정규식 불허 상태를 두고 검증이 그것을 먼저 보게 하는 것이다.

검증 환경

OpenJDK : 21.0.12 MongoDB : 8.0.16 단독 서버 확인 방식 : 팩토리 값과 검증 경로 실행, 실제 서버에 질의 실행 소스 수정 : x

재현 조건

  1. 두 팩토리가 만드는 값과 컴팩트 생성자의 길이 검사를 읽는다.
  2. 검증 메서드의 네 검사와 길이 비교를 읽는다.
  3. 앵커 문자와 빈 플래그, 앵커 문자와 i 플래그, 빈 문자열, 다른 한 글자, 두 글자, 두 도우미의 최단 결과를 금지 정책에 통과시킨다.
  4. 길이 검사를 얹은 인스턴스 도우미를 서로 다른 입력으로 부른다.
  5. 기본 연산자 집합에 정규식이 있는지 확인한다.
  6. 정책을 갈아 끼우고 어떤 필드에 정규식 연산자를 등록해 질의를 조립한다.
  7. 문자열과 비문자열과 필드 없는 문서를 섞어 넣고 그 질의를 실제 서버에서 실행한다.
  8. 이 팩토리와 정책 교체 메서드와 질의 정책 생성을 부르는 곳을 전수로 센다.

본문

정규식 정책은 최대 길이와 허용 플래그와 앵커 요구 세 자리다. 금지를 뜻하는 자리는 없고, 금지 팩토리는 첫 자리를 1 로 잡는다.

0 이 아니라 1 인 이유

:::evidence key="a06-f011-mongoregexpolicy-forbidden-probe" alt="금지 정책과 기본 정책의 값, 최대 길이 0 인 정책이 생성자에서 거부되는 결과, 패턴과 플래그 일곱 조합을 금지 정책의 검증에 통과시킨 결과와 각각의 거절 사유, 길이 검사를 얹은 인스턴스 도우미가 서로 다른 입력에서 던지는 결과, 기본 연산자 집합에 정규식이 없다는 확인, 조립된 질의의 문서, 그리고 문자열과 비문자열과 필드 없는 문서를 섞어 넣은 실제 서버에서 그 질의가 매치한 것과 매치하지 않은 것을 출력한 터미널 기록." caption="최대 길이 0 은 생성자가 거부 · 통과하는 조합은 앵커 문자와 빈 플래그 하나 · 인스턴스 도우미는 입력이 무엇이든 길이에서 거절 · 기본 연산자 집합에 REGEX 없음 · 저장 8건에 매치 4건, 매치된 것은 전부 문자열 값 — 33줄 · exit 0" zoom="true" :::

/** A policy that forbids regular expressions entirely. */
public static MongoRegexPolicy forbidden() {
  return new MongoRegexPolicy(1, Set.of(), true);
}

같은 레코드의 컴팩트 생성자가 0 이하를 막는다. 음수 길이를 오타로 보는 검사인데, 정규식 없음을 뜻할 수 있는 값도 같이 막는다.

길이 0 정책  : IllegalArgumentException: a regex policy needs a positive maximum length

통과하는 조합이 하나 있다

검증은 길이·플래그·앵커·중첩 수량자를 이 순서로 검사한다. 길이 1 이하와 앵커 요구를 함께 만족하는 문자열은 하나뿐이고, 허용 플래그 집합이 비어 있으므로 플래그 자리도 빈 문자열이어야 한다.

  ("^", "") -> 수용
  ("^", "i") -> 거절, regex flag 'i' is not allowed by this collection's policy
  ("", "") -> 거절, the pattern is not anchored at the start, so it cannot use an index and will scan the collection
  ("a", "") -> 거절, the pattern is not anchored at the start, so it cannot use an index and will scan the collection
  ("^a", "") -> 거절, the pattern is 2 characters, above the limit of 1

그 짝을 넘겨 질의를 조립하면 이런 문서가 나온다.

{"$and": [{"name": {"$regularExpression": {"pattern": "^", "options": ""}}}]}

매치되는 것은 그 필드의 문자열 값이다

문자열과 비문자열과 필드 없는 문서를 섞어 여덟 건을 넣고 실행한 결과다.

[실행] 저장 8건 · 매치 4건
  매치   {"name": "ana"}
  매치   {"name": ""}
  매치   {"name": "\ud83d\ude00"}
  매치   {"name": ["zz"]}
  미매치 {"name": 12345}
  미매치 {"name": true}
  미매치 {"name": null}
  미매치 {"other": "x"}

빈 문자열도 이모지도 배열 안의 문자열도 걸린다. 숫자와 불리언과 널과 필드 부재는 걸리지 않는다. 정규식 검색의 대상이 되는 값만 골라서 전부 매치한다는 뜻이다.

두 도우미는 어떤 입력으로도 통과할 수 없다

두 이스케이프 도우미는 호출자의 텍스트를 인용부호 안에 그대로 넣으므로 호출자가 문법을 기여할 수 없다. 출력 길이는 입력을 따라 늘고, 최단이 다섯 자와 네 자다.

  literalPrefix("")    = ^\Q\E  (5자)
  prefixPattern("")    -> 거절, the pattern is 5 characters, above the limit of 1
  literalPrefix("ab")    = ^\Qab\E  (7자)
  prefixPattern("ab")    -> 거절, the pattern is 7 characters, above the limit of 1
  escapedContains("")  = \Q\E  (4자)

금지 정책이 막는 것은 문법을 받지 않는 두 경로이고, 남기는 것은 문법을 그대로 받는 경로의 한 짝이다.

호출 지점은 선언 자리뿐이다

:::evidence key="a06-f011-mongoregexpolicy-forbidden" alt="정규식 정책의 두 팩토리가 만드는 값, 검증 메서드의 네 검사와 길이 비교, 금지 팩토리를 부르는 곳 전수와 정책 교체 메서드를 부르는 곳 전수, 질의 정책을 만드는 곳 전수, 그리고 자동 구성이 질의 계열에서 만드는 빈을 출력한 터미널 기록." caption="금지 팩토리는 길이 1·플래그 없음·앵커 요구 참 · 검증 네 검사 어디에도 정규식 자체를 막는 갈래가 없음 · 금지 팩토리 호출 0 · 정책 교체는 시험 한 곳에서 기본 정책으로 · 질의 정책 생성 다섯 곳은 전부 시험 · 질의 계열의 빈은 예산 집행기 하나 — 58줄 · exit 0" zoom="true" :::

기록의 뒤 네 갈래가 호출 지점을 센다. forbidden() 은 선언 자리 말고 없고, withRegexPolicy 는 시험 한 곳이 기본 정책으로 부르며, 질의 정책을 만드는 다섯 곳은 전부 시험이고, 자동 구성이 질의 계열에서 등록하는 빈은 MongoBudgetEnforcer 하나다.

배선해도 곧장 도달하지는 않는다. 기본 연산자 집합에 정규식이 없어서, 어떤 필드가 withOperators 로 정규식을 명시해 등록해야 한다.

[전제] 기본 연산자 집합에 REGEX 가 있는가 : false  [GT, GTE, LT, IN, EQ, LTE, EXISTS]

필요한 것은 두 선택이 겹치는 일이다.

확인하지 못한 것

이 팩토리를 부르는 배포는 없으므로, 어떤 필드가 정규식 연산자를 등록할지는 다루지 않았다. 확인한 것은 두 선택이 겹쳤을 때 검증이 무엇을 통과시키고 그 결과가 서버에서 무엇을 매치하는지다.