fix(team): bound json merge patch recursion depth

apply_json_merge_patch recurses into nested objects, which the repo's recursive_detector code-quality check flags because unbounded recursion over caller-supplied JSON has caused CPU/stack issues before. Cap the recursion at a depth far above any realistic team-metadata shape and reject deeper patches with a ValueError so a pathologically nested body fails closed instead of overflowing the stack, then register the function in the detector's ignore list alongside the other depth-bounded JSON walkers
This commit is contained in:
Yuneng Jiang 2026-07-11 09:36:15 -07:00
parent 472b64cc62
commit 6875ca00d0
No known key found for this signature in database
3 changed files with 40 additions and 4 deletions

View file

@ -2,20 +2,32 @@
from pydantic import JsonValue
# A merge patch recurses as deep as the client's JSON nests. Cap it far above any
# realistic team-metadata shape but well below Python's stack limit, so a
# pathologically deep patch is rejected instead of overflowing the stack.
_MAX_MERGE_DEPTH = 64
def apply_json_merge_patch(target: JsonValue, patch: JsonValue) -> JsonValue:
def apply_json_merge_patch(target: JsonValue, patch: JsonValue, _depth: int = 0) -> JsonValue:
"""Apply an RFC 7386 JSON Merge Patch to ``target`` and return the result.
- a key absent from ``patch`` keeps its value in ``target``
- a key mapped to ``null`` in ``patch`` is removed from the result
- any other value overwrites, recursing into nested objects
``target`` is never mutated; a new value is returned.
``target`` is never mutated; a new value is returned. Raises ``ValueError``
if ``patch`` nests deeper than ``_MAX_MERGE_DEPTH``.
"""
if not isinstance(patch, dict):
return patch
if _depth >= _MAX_MERGE_DEPTH:
raise ValueError(f"JSON merge patch nesting exceeds the maximum depth of {_MAX_MERGE_DEPTH}")
base = target if isinstance(target, dict) else {}
preserved = {key: value for key, value in base.items() if key not in patch}
applied = {key: apply_json_merge_patch(base.get(key), value) for key, value in patch.items() if value is not None}
applied = {
key: apply_json_merge_patch(base.get(key), value, _depth + 1)
for key, value in patch.items()
if value is not None
}
return {**preserved, **applied}

View file

@ -53,6 +53,7 @@ IGNORE_FUNCTIONS = [
"resolve_oci_schema_anyof", # OCI: bounded by JSON-schema tree depth (no cycles possible in well-formed input).
"sanitize_oci_schema", # OCI: bounded by JSON-schema tree depth.
"_freeze_for_dedupe", # OTEL: max depth set (default 16, _FREEZE_MAX_DEPTH); fails closed by returning repr(value) at the cap.
"apply_json_merge_patch", # max depth set (_MAX_MERGE_DEPTH=64); fails closed by raising ValueError at the cap.
]

View file

@ -2,7 +2,7 @@ import copy
import pytest
from litellm.proxy.common_utils.json_merge_patch import apply_json_merge_patch
from litellm.proxy.common_utils.json_merge_patch import _MAX_MERGE_DEPTH, apply_json_merge_patch
# RFC 7386 Appendix A — the normative test suite for JSON Merge Patch.
# https://www.rfc-editor.org/rfc/rfc7386#appendix-A
@ -69,3 +69,26 @@ def test_scalar_patch_replaces_object_wholesale():
def test_object_patch_over_non_object_target_starts_from_empty():
assert apply_json_merge_patch("not-an-object", {"a": 1, "b": None}) == {"a": 1}
def _nest(levels: int) -> dict:
"""A patch nested ``levels`` dicts deep with a scalar leaf at the bottom."""
value: object = "leaf"
for _ in range(levels):
value = {"a": value}
return value # type: ignore[return-value]
def test_merge_within_max_depth_is_allowed():
"""A deeply-but-not-pathologically nested patch merges without raising."""
result = apply_json_merge_patch({}, _nest(_MAX_MERGE_DEPTH - 1))
for _ in range(_MAX_MERGE_DEPTH - 1):
result = result["a"]
assert result == "leaf"
def test_merge_beyond_max_depth_raises():
"""A patch nested past the cap fails closed (ValueError) rather than
overflowing the Python stack — the guard the recursion detector requires."""
with pytest.raises(ValueError, match="maximum depth"):
apply_json_merge_patch({}, _nest(_MAX_MERGE_DEPTH + 5))