Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Fixed

- Cap LLM-judge response JSON nesting at 32 levels before parse to prevent recursive-object resource exhaustion.
- Public fixed-form `assemble_test_form` delegates greedy maximum-information selection and content-feasibility look-ahead to the Rust core (`assemble_test_form_greedy`).
- Public fixed-anchor `link_fixed_item_parameters` delegates affine scale/shift estimation and parameter transformation to the Rust core.
- Public `observed_information` and `second_order_test` delegate Hessian assembly and eigenvalue diagnostics to the Rust core.
Expand Down
6 changes: 6 additions & 0 deletions docs/changelog.d/764-llm-judge-json-depth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Fixed

#### LLM judge JSON nesting depth bound

- Cap raw LLM-judge response JSON nesting at 32 levels before `json.loads`, failing closed with `JudgeFormatError` so hostile recursive objects cannot expand into parser resource exhaustion.
- Keep valid shallow judge payloads accepted with the existing criterion/score contracts.
16 changes: 16 additions & 0 deletions docs/doctoring/llm_judge_json_depth_bound.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# LLM judge JSON nesting depth bound

## Standard / threat model

Open Web Application Security Project. (2021). *OWASP API security top 10 2023: API4 โ€” unrestricted resource consumption*. OWASP Foundation. https://owasp.org/API-Security/

Bray, T. (Ed.). (2017). *The JavaScript Object Notation (JSON) data interchange format* (RFC 8259). Internet Engineering Task Force. https://doi.org/10.17487/RFC8259

## Product application

Automated LLM-judge responses are untrusted text. Before `json.loads`, the product walks the raw response character stream, tracks object/array nesting outside string literals, and rejects nesting deeper than 32 with `JudgeFormatError`. This bounds parser stack/heap growth for recursive structures while preserving legitimate shallow score objects used by Contextual Orchestrator judges.

## Verification

- `tests/test_llm_judge.py::test_judge_rejects_excessive_json_nesting`
- `tests/test_llm_judge.py::test_judge_accepts_bounded_json_nesting`
25 changes: 25 additions & 0 deletions python/fast_mlsirm/llm_judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
MAX_JUDGE_TEXT_CHARACTERS = 200_000
MAX_JUDGE_CRITERIA = 32
MAX_JUDGE_CATEGORIES = MAX_POLYTOMOUS_CATEGORIES
MAX_JUDGE_JSON_DEPTH = 32
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$")


Expand Down Expand Up @@ -272,8 +273,32 @@ def _criteria(values: Iterable[JudgeCriterion | Mapping[str, Any]]) -> tuple[Jud
return tuple(normalized)


def _validate_raw_json_depth(content: str) -> None:
depth = 0
in_string = False
escaped = False
for char in content:
if in_string:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
in_string = False
continue
if char == '"':
in_string = True
elif char in "[{":
depth += 1
if depth > MAX_JUDGE_JSON_DEPTH:
raise JudgeFormatError(f"judge response JSON nesting exceeds maximum depth of {MAX_JUDGE_JSON_DEPTH}")
elif char in "]}":
depth -= 1


def _response_object(raw: str) -> dict[str, Any]:
text = raw.strip()
_validate_raw_json_depth(text)
try:
value = json.loads(text)
except json.JSONDecodeError as exc:
Expand Down
31 changes: 31 additions & 0 deletions tests/test_llm_judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,34 @@ def test_judge_criteria_reject_non_contract_values_with_value_error() -> None:
test_judge_criteria_reject_invalid_runtime_types()
test_judge_criteria_reject_non_contract_values_with_value_error()
print("ok")

def test_judge_rejects_excessive_json_nesting() -> None:
"""Deeply nested JSON cannot expand into recursive parser DoS."""
# Nesting depth 33 exceeds MAX_JUDGE_JSON_DEPTH (32).
nested = "{" + '"k":{' * 32 + '"score": 0.8' + "}" * 32 + "}"
assert nested.count("{") == 33
with pytest.raises(JudgeFormatError, match="nesting exceeds maximum depth"):
ContextualOrchestratorJudge(_FakeOrchestrator(nested)).judge(
task="task",
answer="answer",
criteria=CRITERIA,
)


def test_judge_accepts_bounded_json_nesting() -> None:
"""Nesting at the admitted depth still parses when the payload is valid."""
# Build a valid judge payload with modest nesting under the limit.
inner = {
"score": 0.8,
"accepted": True,
"rationale": "ok",
"criterion_scores": {"task_alignment": 0.8, "factual_support": 0.8},
}
raw = json.dumps(inner)
result = ContextualOrchestratorJudge(_FakeOrchestrator(raw)).judge(
task="task",
answer="answer",
criteria=CRITERIA,
)
assert result.score == 0.8

Loading