From 7e8372809b68689fb3742fa3de5231fdae2deeb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 21:13:51 +0000 Subject: [PATCH 1/3] Restrict state restore to declared dataclass fields update_dataclass_from_json only rejected dunder keys and unhashable class-level defaults, but hasattr/getattr/setattr in _recursive_update_dataclass_from_json_obj still resolved ordinary (non-dunder) keys through class-attribute fallback. A state class holding a nested object with a hashable-but-mutable class-level mapping (e.g. a dict subclass with a custom __hash__) let a crafted /__ui__ UserEvent payload mutate that shared, class-level object directly, affecting every session rather than just the current one. Add a check that a non-dunder key resolved via hasattr must also be one of the target's declared __dataclass_fields__, unless the target is itself a plain dict (dict-typed state fields still restore arbitrary keys as before). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MC1XyvNKytmVvBSgtPqJpv --- mesop/dataclass_utils/dataclass_utils.py | 8 +++++ mesop/dataclass_utils/dataclass_utils_test.py | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/mesop/dataclass_utils/dataclass_utils.py b/mesop/dataclass_utils/dataclass_utils.py index 502ce63a7..42297aaa6 100644 --- a/mesop/dataclass_utils/dataclass_utils.py +++ b/mesop/dataclass_utils/dataclass_utils.py @@ -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): diff --git a/mesop/dataclass_utils/dataclass_utils_test.py b/mesop/dataclass_utils/dataclass_utils_test.py index f04a7a1c4..f027b2dc1 100644 --- a/mesop/dataclass_utils/dataclass_utils_test.py +++ b/mesop/dataclass_utils/dataclass_utils_test.py @@ -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__ # noqa: A003 (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__])) From f771c10c699752a25159b611041fc35915cb2c77 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 21:24:48 +0000 Subject: [PATCH 2/3] Fix ruff lint failure: drop unnecessary noqa comment The A003 rule isn't in this project's ruff select list, so the noqa was flagged as unused by the pinned ruff pre-commit hook. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MC1XyvNKytmVvBSgtPqJpv --- mesop/dataclass_utils/dataclass_utils_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesop/dataclass_utils/dataclass_utils_test.py b/mesop/dataclass_utils/dataclass_utils_test.py index f027b2dc1..fd8c4a2c7 100644 --- a/mesop/dataclass_utils/dataclass_utils_test.py +++ b/mesop/dataclass_utils/dataclass_utils_test.py @@ -621,7 +621,7 @@ def test_class_level_attribute_pollution_blocked(): """ class MutableRoleMap(dict): - __hash__ = object.__hash__ # noqa: A003 (mimic a hashable-but-mutable gadget) + __hash__ = object.__hash__ # (mimic a hashable-but-mutable gadget) class RoleService: # Class-level (not annotated, so not a dataclass field) shared mapping. From 9b41d9ac1c74478cc448473dde2832e21d27cf79 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 21:37:08 +0000 Subject: [PATCH 3/3] Fix pyright type error on test's dict subclass __hash__ override typeshed types dict.__hash__ as None, so assigning object.__hash__ (a real function) to reinstate hashability needs a type: ignore. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MC1XyvNKytmVvBSgtPqJpv --- mesop/dataclass_utils/dataclass_utils_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesop/dataclass_utils/dataclass_utils_test.py b/mesop/dataclass_utils/dataclass_utils_test.py index fd8c4a2c7..c413cb18d 100644 --- a/mesop/dataclass_utils/dataclass_utils_test.py +++ b/mesop/dataclass_utils/dataclass_utils_test.py @@ -621,7 +621,7 @@ def test_class_level_attribute_pollution_blocked(): """ class MutableRoleMap(dict): - __hash__ = object.__hash__ # (mimic a hashable-but-mutable gadget) + __hash__ = object.__hash__ # type: ignore (mimic a hashable-but-mutable gadget) class RoleService: # Class-level (not annotated, so not a dataclass field) shared mapping.