Security: secure by default¶
Loading a statechart compiles its guards, datamodel expressions and executable content into
callables. Because a document can come from an untrusted source, the IO layer is secure by
default, mirroring the discipline of yaml.safe_load.
The trusted flag controls only expression and script evaluation. Document parsing is
always safe: YAML is read with safe-load semantics and JSON with the stdlib parser, so a
document can never instantiate arbitrary Python objects, regardless of trusted.
The two evaluation modes¶
load(..., trusted=False) (the default) uses a restricted evaluator built on an AST
whitelist. It allows the everyday building blocks of a guard or expression:
comparisons (
==,!=,<,>=, …) and boolean algebra (and,or,not);arithmetic, indexing (
items[0]), and list/tuple/set/dict literals;reading attributes, including property getters (
order.is_ready);the
In(state_id)predicate for testing the active configuration.
It refuses anything that could escape the sandbox:
function and method calls (the only exception is
In(...));builtins (
len,open,__import__, …), lambdas and comprehensions;dunder or private attribute access (
x.__class__,x._secret).
A rejected expression fails at load time with InvalidDefinition, not later at runtime:
>>> from statemachine.io import load
>>> from statemachine.exceptions import InvalidDefinition
>>> doc = """
... states:
... s:
... initial: true
... transitions:
... - {event: go, target: s, cond: "escape_to_shell()"}
... """
>>> try:
... load(doc, format="yaml")
... except InvalidDefinition:
... print("rejected")
rejected
What the whitelist refuses is call syntax (name(), obj.method()), so an attacker’s
expression cannot reach builtins, the one exception being In(...):
cond: "order.is_ready"— allowed (a plain attribute read, which runs the property getter).cond: "order.is_ready()"— rejected (call syntax).
Guards (cond/unless) have one extra affordance, matching class-defined guards: a bare name
that resolves to a model method is invoked by name, like a callback reference, receiving
the event’s keyword arguments. That is still safe, it runs the integrator’s own method named
in the document, not arbitrary code, so guard logic can be a property, an attribute or a
method without trusted=True.
What trusted=True unlocks¶
load(..., trusted=True) evaluates expressions as full Python (eval) and enables the
script action (exec). Concretely, it adds exactly what the restricted mode withholds:
method calls and builtins inside guards and expressions (
len(cart),order.is_ready(),_event.data.get("x"));the
scriptaction, a block of Python statements that reads and writes the model.
Everything else, the assign, raise, send, log, foreach, cancel and if actions,
already works in both modes; script is the only action gated behind trusted. Errors in
trusted expressions surface at runtime (as error.execution) rather than at load time.
Use trusted=True only for documents you fully control.
Background¶
This safe-by-default behaviour comes from the security advisory
GHSA-v4jc-pm6r-3vj8
(CVE-2026-47103).
Before it, SCXML datamodel expressions were evaluated with eval, which let a malicious
document run arbitrary code on load. The restricted evaluator removes that by default across
every format (SCXML, JSON and YAML).