mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
parent
472b64cc62
commit
6875ca00d0
3 changed files with 40 additions and 4 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue