Skip to content

Restrict state restore to declared dataclass fields - #1399

Merged
richard-to merged 3 commits into
mainfrom
claude/mesop-security-advisory-bedyv2
Aug 16, 2026
Merged

Restrict state restore to declared dataclass fields#1399
richard-to merged 3 commits into
mainfrom
claude/mesop-security-advisory-bedyv2

Conversation

@richard-to

Copy link
Copy Markdown
Collaborator

Summary

  • A security advisory reported that update_dataclass_from_json (via /__ui__ UserEvent state blobs) blocks dunder keys and unhashable class-level defaults, but ordinary non-dunder keys can still reach hashable, mutable class-level attributes through normal hasattr/getattr/setattr fallback — letting a crafted state payload mutate an object shared across all sessions (e.g. a dict subclass with a custom __hash__ used as a class attribute on a nested state object).
  • I reproduced the chain against current main and confirmed it's valid: the mutation lands during update_state() in mesop/runtime/context.py, which runs before the handler-id lookup, so the developer error returned afterward doesn't roll back the damage.
  • Fix: in _recursive_update_dataclass_from_json_obj, a non-dunder key resolved via hasattr is now also required to be a declared __dataclass_fields__ entry on the target — unless the target is a plain dict (dict-typed state fields, e.g. dict[str, bool], still restore arbitrary keys as before).

Test plan

  • Added test_class_level_attribute_pollution_blocked reproducing the advisory's RoleService/MutableRoleMap PoC, asserting the update is rejected and the shared class-level mapping is left untouched.
  • Verified existing patterns still work by exercising the module directly (couldn't run full bazel/pytest suite in this sandbox — no bazel/protoc available to generate mesop.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.
  • CI should run the full test suite on this PR.

Generated by Claude Code

claude added 3 commits August 14, 2026 21:13
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
@richard-to
richard-to merged commit c993b01 into main Aug 16, 2026
3 checks passed
@Vectrain51

Copy link
Copy Markdown

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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants