Restrict state restore to declared dataclass fields - #1399
Merged
Conversation
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MC1XyvNKytmVvBSgtPqJpv
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MC1XyvNKytmVvBSgtPqJpv
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MC1XyvNKytmVvBSgtPqJpv
|
Thanks for preparing this patch and the regression test. I reviewed the change, and it addresses the remaining non-dunder class-level attribute pollution path. As a small follow-up, please ensure the existing tests for valid nested dataclass fields and dictionary-valued state fields continue to pass, since those updates should remain supported. Subject to the normal CI results, this looks like an appropriate fix. Thanks again for addressing the residual issue :) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
update_dataclass_from_json(via/__ui__UserEventstate blobs) blocks dunder keys and unhashable class-level defaults, but ordinary non-dunder keys can still reach hashable, mutable class-level attributes through normalhasattr/getattr/setattrfallback — letting a crafted state payload mutate an object shared across all sessions (e.g. adictsubclass with a custom__hash__used as a class attribute on a nested state object).mainand confirmed it's valid: the mutation lands duringupdate_state()inmesop/runtime/context.py, which runs before the handler-id lookup, so the developer error returned afterward doesn't roll back the damage._recursive_update_dataclass_from_json_obj, a non-dunder key resolved viahasattris now also required to be a declared__dataclass_fields__entry on the target — unless the target is a plaindict(dict-typed state fields, e.g.dict[str, bool], still restore arbitrary keys as before).Test plan
test_class_level_attribute_pollution_blockedreproducing the advisory'sRoleService/MutableRoleMapPoC, asserting the update is rejected and the shared class-level mapping is left untouched.bazel/pytestsuite in this sandbox — no bazel/protoc available to generatemesop.protos.ui_pb2): nested dataclasses, lists of dataclasses, dict/nested-dict-typed fields, unannotated nested state classes with declared fields, and the existing dunder-key protection.Generated by Claude Code