From dec99f313632aec33adad028f61eb5ddc717ef28 Mon Sep 17 00:00:00 2001 From: Fernando Macedo Date: Sat, 1 Aug 2026 14:12:36 -0300 Subject: [PATCH 1/2] docs: remove em dashes introduced in the 3.2.1 security notes Signed-off-by: Fernando Macedo --- docs/io/security.md | 12 ++++++------ docs/releases/3.2.1.md | 4 ++-- statemachine/spec_parser.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/io/security.md b/docs/io/security.md index a121fceb..8984b5bd 100644 --- a/docs/io/security.md +++ b/docs/io/security.md @@ -201,17 +201,17 @@ every format (SCXML, JSON and YAML). A set of follow-up advisories hardened the restricted mode further and prompted the confidentiality/integrity vs availability framing above: -- [GHSA-fj3w-533r-fvf6](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-fj3w-533r-fvf6) - — `` and `` read local files during loading, regardless of +- [GHSA-fj3w-533r-fvf6](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-fj3w-533r-fvf6): + `` and `` read local files during loading, regardless of `trusted`. Loading now rejects external `src` references unless `trusted=True`, and refuses ``/DTD to block XML entity-expansion bombs. - [GHSA-v3qq-3xvg-m77g](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-v3qq-3xvg-m77g) - / [GHSA-4857-ggqc-p3jc](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-4857-ggqc-p3jc) - — a document could write to a dunder/private/protected attribute (notably traversing + / [GHSA-4857-ggqc-p3jc](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-4857-ggqc-p3jc): + a document could write to a dunder/private/protected attribute (notably traversing `__class__`) and corrupt the shared model class process-wide. Write targets are now confined to public model attributes on every path segment. -- [GHSA-r8gj-366q-cgvj](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-r8gj-366q-cgvj) - — `**`/`*` in the restricted evaluator had no magnitude bound, so a tiny expression could +- [GHSA-r8gj-366q-cgvj](https://github.com/fgmacedo/python-statemachine/security/advisories/GHSA-r8gj-366q-cgvj): + `**`/`*` in the restricted evaluator had no magnitude bound, so a tiny expression could exhaust CPU or memory. They are now magnitude-capped. These were released together in 3.2.1. diff --git a/docs/releases/3.2.1.md b/docs/releases/3.2.1.md index ddd3c406..0ce4d923 100644 --- a/docs/releases/3.2.1.md +++ b/docs/releases/3.2.1.md @@ -15,9 +15,9 @@ See [](../io/security.md). ```{note} **Am I affected?** -- **Yes** — if you load documents you did **not** author (via `statemachine.io.load(...)` / +- **Yes**, if you load documents you did **not** author (via `statemachine.io.load(...)` / `build_processor(...)` / `SCXMLProcessor`) with the default `trusted=False`. -- **No** — if you define machines in Python, only load documents you wrote yourself, or already +- **No**, if you define machines in Python, only load documents you wrote yourself, or already load with `trusted=True` for fully controlled documents. **Affected versions:** `>= 3.2.0, < 3.2.1` (this attack surface shipped with the diff --git a/statemachine/spec_parser.py b/statemachine/spec_parser.py index 4cf4c285..1046a423 100644 --- a/statemachine/spec_parser.py +++ b/statemachine/spec_parser.py @@ -289,7 +289,7 @@ def recurse(child): op_type = type(node.op) if op_type not in binary_operators: # e.g. bitwise ``^``/``|``/``<<`` are outside the allowlist. (``**`` and ``*`` - # are allowed but magnitude-capped — see ``binary_operators``.) + # are allowed but magnitude-capped, see ``binary_operators``.) raise ValueError(f"Binary operator '{op_type.__name__}' is not allowed") return build_binop(binary_operators[op_type], recurse(node.left), recurse(node.right)) case ast.List(elts=elts) if allow_value_nodes: From b6b6c149cfb3f60f3b4144718f894c3822ff229b Mon Sep 17 00:00:00 2001 From: Fernando Macedo Date: Sat, 1 Aug 2026 14:30:26 -0300 Subject: [PATCH 2/2] test: make add_note class-pollution assertion Python 3.10-compatible BaseException.add_note only exists on Python 3.11+ (PEP 678), so asserting callable(TransitionNotAllowed.add_note) raised AttributeError on 3.10. Assert on the injection site (the class __dict__) instead, which is version-agnostic and directly checks the blocked exploit did not set add_note = 1. Signed-off-by: Fernando Macedo --- tests/io/test_security.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/io/test_security.py b/tests/io/test_security.py index 79370d23..dc33d16e 100644 --- a/tests/io/test_security.py +++ b/tests/io/test_security.py @@ -551,11 +551,14 @@ def test_shared_exception_class_not_corrupted(self, fmt): try: sm = _run_exec(scxml, native, fmt) assert "failed" in _config(sm) - # The shared class is intact: add_note is still the inherited method, not int 1, - # and a normal exception still constructs and carries a note. - assert callable(TransitionNotAllowed.add_note) - err = TransitionNotAllowed(None, set()) - err.add_note("still works") + # The shared class is intact: the exploit did not inject ``add_note = 1`` onto it. + # ``BaseException.add_note`` only exists on Python 3.11+, so assert on the injection + # site (the class ``__dict__``) rather than the inherited method, to stay + # version-agnostic. + assert "add_note" not in TransitionNotAllowed.__dict__ + assert getattr(TransitionNotAllowed, "add_note", None) != 1 + # A normal exception still constructs. + TransitionNotAllowed(None, set()) finally: # Defensive: if a regression ever mutated the shared class, restore it so the # rest of the suite is not corrupted.