mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
fix: bound recursive form serialization
Reject excessively nested multipart values before recursion can exhaust the process stack. This also restores the code-quality gate broken by the unbounded walkers.
This commit is contained in:
parent
ca0b951a43
commit
f1a499acfa
3 changed files with 26 additions and 8 deletions
|
|
@ -2,6 +2,7 @@ from collections.abc import Mapping
|
|||
from typing import Final
|
||||
|
||||
import litellm
|
||||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
|
||||
|
||||
|
||||
def _form_field_value(value: object) -> str:
|
||||
|
|
@ -12,13 +13,17 @@ def _form_field_value(value: object) -> str:
|
|||
return str(value)
|
||||
|
||||
|
||||
def _flatten_form_field(key: str, value: object) -> tuple[tuple[str, str], ...]:
|
||||
def _flatten_form_field(key: str, value: object, depth: int = 0) -> tuple[tuple[str, str], ...]:
|
||||
if depth > DEFAULT_MAX_RECURSE_DEPTH:
|
||||
raise ValueError("Form field value exceeds maximum nesting depth")
|
||||
if isinstance(value, Mapping):
|
||||
return tuple(
|
||||
item for subkey, subvalue in value.items() for item in _flatten_form_field(f"{key}[{subkey}]", subvalue)
|
||||
item
|
||||
for subkey, subvalue in value.items()
|
||||
for item in _flatten_form_field(f"{key}[{subkey}]", subvalue, depth + 1)
|
||||
)
|
||||
if isinstance(value, (list, tuple)):
|
||||
return tuple(item for entry in value for item in _flatten_form_field(f"{key}[]", entry))
|
||||
return tuple(item for entry in value for item in _flatten_form_field(f"{key}[]", entry, depth + 1))
|
||||
if value is None:
|
||||
return ()
|
||||
serialized: Final = _form_field_value(value)
|
||||
|
|
@ -31,18 +36,20 @@ def _is_form_scalar(value: object) -> bool:
|
|||
return value is not None and not isinstance(value, (Mapping, list, tuple))
|
||||
|
||||
|
||||
def _flatten_form_data_field(key: str, value: object) -> tuple[tuple[str, str | tuple[str, ...]], ...]:
|
||||
def _flatten_form_data_field(key: str, value: object, depth: int = 0) -> tuple[tuple[str, str | tuple[str, ...]], ...]:
|
||||
if depth > DEFAULT_MAX_RECURSE_DEPTH:
|
||||
raise ValueError("Form field value exceeds maximum nesting depth")
|
||||
if isinstance(value, Mapping):
|
||||
return tuple(
|
||||
item
|
||||
for subkey, subvalue in value.items()
|
||||
for item in _flatten_form_data_field(f"{key}[{subkey}]", subvalue)
|
||||
for item in _flatten_form_data_field(f"{key}[{subkey}]", subvalue, depth + 1)
|
||||
)
|
||||
if isinstance(value, (list, tuple)):
|
||||
if all(_is_form_scalar(entry) for entry in value):
|
||||
serialized_fields: Final = tuple(field for entry in value if (field := _form_field_value(entry)))
|
||||
return ((key, serialized_fields),) if serialized_fields else ()
|
||||
return tuple(item for entry in value for item in _flatten_form_data_field(f"{key}[]", entry))
|
||||
return tuple(item for entry in value for item in _flatten_form_data_field(f"{key}[]", entry, depth + 1))
|
||||
if value is None:
|
||||
return ()
|
||||
serialized: Final = _form_field_value(value)
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ IGNORE_FUNCTIONS = [
|
|||
"json_string_leaves", # max depth set (MAX_STRUCTURED_CONTENT_SCAN_DEPTH); fails closed by raising at the cap so nothing goes unscanned.
|
||||
"with_json_string_leaves", # transitively bounded: only runs on a tree json_string_leaves already walked under the cap.
|
||||
"json_unrewritable_labels", # max depth set (MAX_STRUCTURED_CONTENT_SCAN_DEPTH); returns the None sentinel at the cap so the caller blocks.
|
||||
"_flatten_form_field", # bounded by the nesting depth of the already-parsed request body (a finite JSON tree, no cycles possible).
|
||||
"_flatten_form_data_field", # bounded by the nesting depth of the already-parsed request body (a finite JSON tree, no cycles possible).
|
||||
"_flatten_form_field", # bounded by DEFAULT_MAX_RECURSE_DEPTH; raises at the cap.
|
||||
"_flatten_form_data_field", # bounded by DEFAULT_MAX_RECURSE_DEPTH; raises at the cap.
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import httpx
|
||||
import pytest
|
||||
|
||||
from litellm.litellm_core_utils.llm_request_utils import (
|
||||
flatten_form_field_values,
|
||||
|
|
@ -97,3 +98,13 @@ def test_flatten_form_field_values_scalar_list_survives_update_into_multipart():
|
|||
|
||||
assert names.count("loras") == 2
|
||||
assert names.count("model") == 1
|
||||
|
||||
|
||||
def test_form_field_serializers_reject_cyclic_values():
|
||||
cyclic: dict[str, object] = {}
|
||||
cyclic["self"] = cyclic
|
||||
|
||||
with pytest.raises(ValueError, match="maximum nesting depth"):
|
||||
flatten_form_field_values({"metadata": cyclic})
|
||||
with pytest.raises(ValueError, match="maximum nesting depth"):
|
||||
serialize_multipart_form_fields({"metadata": cyclic})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue