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
8 changes: 8 additions & 0 deletions mesop/dataclass_utils/dataclass_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ def _recursive_update_dataclass_from_json_obj(instance: Any, json_dict: Any):
raise MesopDeveloperException(
f"Cannot use dunder property: {key} in stateclass"
)
if (
not isinstance(instance, dict)
and hasattr(instance, key)
and key not in getattr(instance, "__dataclass_fields__", {})
):
raise MesopDeveloperException(
f"Cannot set non-dataclass-field property: {key} in stateclass"
)
if hasattr(instance, key):
attr = getattr(instance, key)
if isinstance(value, dict):
Expand Down
32 changes: 32 additions & 0 deletions mesop/dataclass_utils/dataclass_utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,5 +612,37 @@ class A:
assert __name__ == initial_name


def test_class_level_attribute_pollution_blocked():
"""Regression test for class-level mutable attribute pollution.

A non-dunder key that resolves to a class-level attribute (rather than a
declared dataclass field) must be rejected, since setting it would mutate
state shared across all sessions instead of the per-instance field.
"""

class MutableRoleMap(dict):
__hash__ = object.__hash__ # type: ignore (mimic a hashable-but-mutable gadget)

class RoleService:
# Class-level (not annotated, so not a dataclass field) shared mapping.
role_map = MutableRoleMap({"assistant": "user"})

@dataclass
class ChatState:
service: RoleService = field(default_factory=RoleService)

state = ChatState()
with pytest.raises(MesopDeveloperException) as exc_info:
update_dataclass_from_json(
state, '{"service": {"role_map": {"assistant": "system"}}}'
)
assert (
"Cannot set non-dataclass-field property: role_map in stateclass"
in str(exc_info.value)
)
# Make sure the shared class-level mapping was not mutated.
assert RoleService.role_map == {"assistant": "user"}


if __name__ == "__main__":
raise SystemExit(pytest.main(["-vv", __file__]))
Loading