Skip to content

refactor(wme-importer): AST-restricted B-UML import — banner-free sectioning + exec() removal (#560) - #574

Draft
ArmenSl wants to merge 3 commits into
developmentfrom
refactor/importer-ast-sectioning
Draft

refactor(wme-importer): AST-restricted B-UML import — banner-free sectioning + exec() removal (#560)#574
ArmenSl wants to merge 3 commits into
developmentfrom
refactor/importer-ast-sectioning

Conversation

@ArmenSl

@ArmenSl ArmenSl commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Completes #560 — move the WME B-UML importers off text/exec() heuristics to AST-restricted parsing. Two parts:

Part 1 — banner-free sectioning

project_to_json used to decide the diagram set by counting # X MODEL # comment banners, coupling the importer to the generator's comment formatting (root cause of WME #161). Now it parses the AST, reads the authoritative Project(models=[...]) list, resolves each model's type from its constructor, and slices each model's source as its dependency closure. One diagram per model in the list; banners are no longer load-bearing. Adds a banner-immunity test (stray/duplicate/renumbered banners can't create phantom diagrams).

Part 2 — exec() removal (the security core)

The class/gui/bpmn/quantum converters reconstructed the model by exec()-ing the uploaded .py in a restricted namespace — escapable (().__class__.__bases__[0].__subclasses__() needs no builtins), i.e. RCE on untrusted upload. Replaced with a shared whitelist AST evaluator (_safe_eval.py):

  • Allows: literals/collections, calls to whitelisted constructors, method-chains on namespace values, enum reads, and the if/try the generated files use.
  • Refuses (UnsafeConstruct, un-catchable by uploaded try/except): any __dunder__ attribute (read/write/call) — the single control defeating __class__/__globals__/__subclasses__/__import__; non-whitelisted bare calls (__import__,getattr,eval); str/bytes-receiver method calls (format-leak gadgets); subscript, lambda, comprehensions, f-strings, imports.
  • Each converter's safe_globals whitelist and JSON walker are unchanged; only the exec step swapped. OCL Constraint expressions round-trip verbatim (opaque strings, never parsed/executed).

Diff & tests

+1207 / −195 across 8 files. exec( real calls in buml_to_json: 0.

  • converters + code builder: 521 passed; full WME tree: 556 passed, 4 skipped — all re-verified locally.
  • New: banner-immunity test; 26 adversarial security payloads (subclass traversal, __import__/getattr-to-globals, str.format leak, lambda/comprehension/subscript…) all refused and proven un-swallowable; OCL constraint round-trip.

Reviewer notes / honest caveats

  • Only cosmetic behavior change: diagram titles now come from each model's name= (verified the class-diagram count is unchanged for auth/User-model projects — an earlier claim of a count change was wrong and removed).
  • The evaluator permits non-dunder method calls on any non-str/bytes namespace value (shared evaluator, not a per-converter method allowlist); the dunder block is the load-bearing control and no dangerous callable is reachable (quantum's blanket module seed exposes only BUML classes — no os/sys/open).
  • Follow-up worth considering: tighten quantum's dir(module) seed to an explicit whitelist so a future stray import in that module can't widen the surface.

Draft → mark ready when you've reviewed. #161 is already fixed & live independently; this is the durable, security-hardening version.

ArmenSl added 2 commits July 8, 2026 17:00
…ot comment banners

project_to_json determined the diagram set by counting '# X MODEL #' comment
banners, coupling the importer to the generator's comment formatting. A
duplicate or renamed banner produced phantom empty diagrams (root cause of
WME #161).

Rework: parse the file AST, read the authoritative Project(models=[...]) list,
resolve each model's diagram type from its constructor, and slice each model's
source as the transitive closure of the statements it references. Exactly one
diagram per model in the list; banners are no longer load-bearing. Object
diagrams keep the domain-code prepend; single-diagram files (no Project
wrapper) keep the existing fallback.

Adds a banner-immunity regression test: stray / duplicate / renumbered banners
must not change the output.

Behavioral notes:
- diagram titles now come from each model's name= argument;
- a project's User reference DomainModel now surfaces as its own ClassDiagram
  instead of being merged into the app class diagram.

Relates to BESSER#560 (move WME converters off exec/text heuristics).
The class/gui/bpmn/quantum B-UML importers reconstructed the model by
exec()-ing the uploaded .py in a restricted namespace. That sandbox is
escapable (().__class__.__bases__[0].__subclasses__() needs no builtins),
so exec() on untrusted upload is an RCE risk however the globals are trimmed.

Add a shared safe AST evaluator (buml_to_json/_safe_eval.py): ast.parse +
a whitelist interpreter allowing only literals/collections, calls to
whitelisted constructors, method-chains on namespace values, enum reads,
and the if/try the generated files rely on. Every attribute access
(read/write/call) is refused when the name is a dunder -- the one control
that defeats the known escapes (__class__/__globals__/__subclasses__/...).
Non-whitelisted bare calls (__import__, getattr, eval), str/bytes-receiver
method calls (format-leak gadgets), subscript, lambda, comprehensions,
f-strings and imports are all refused; UnsafeConstruct cannot be swallowed
by an uploaded try/except.

The four converters call safe_exec() instead of exec(); their safe_globals
whitelists and JSON walkers are unchanged. OCL Constraint expressions
round-trip verbatim (opaque strings, never parsed/executed).

Tests: shared-evaluator suite with 26 adversarial payloads (all refused,
un-catchable) + OCL constraint round-trip; full WME suite green.

Completes the exec() removal for the buml_to_json importers (BESSER#560).
@ArmenSl ArmenSl changed the title refactor(wme-importer): AST + models-list sectioning (drop comment-banner coupling) refactor(wme-importer): AST-restricted B-UML import — banner-free sectioning + exec() removal (#560) Jul 9, 2026
domain_model_to_code emitted types={} / associations={} / generalizations={}
for empty collections. {} is a dict, but the metamodel's setters expect a set,
so re-importing an empty domain model crashed with 'unsupported operand type(s)
for |: dict and set'. Emit set() instead, so empty models round-trip.

Updates the test that previously documented this as a known limitation to
assert the model now both compiles and exec()s.
@ArmenSl

ArmenSl commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

End-to-end validation against a local backend

Ran the branch backend locally and drove real import/export + OCL through the HTTP endpoints:

  • Multi-object round-trip (the Add association icons in the palette of the web mdoeling editor #161 case): 20/20 exact — projects with 1→20 object models each re-import to precisely that many object diagrams (no phantoms, no drops).
  • OCL validation (/validate-diagram, an OCL class diagram): 200, no errors — OCL evaluation unaffected.
  • Agent / BPMN / state-machine template round-trips: OK.

Follow-up fix included here (0d940673)

While testing I found a real, pre-existing round-trip bug and fixed it: domain_model_to_code emitted types={} / associations={} / generalizations={} for empty collections — {} is a dict, but the metamodel's setters expect a set, so an empty domain model crashed on re-import ({} | set). Now emits set(). The test that documented this as a "known limitation" is updated to assert it compiles and exec()s.

Not bugs (investigated, ruled out)

  • The class-diagram template round-trips fail because those templates are in the React-Flow (migration-frontend) node format ({type:"class", data:{...}}), which the development backend's parser doesn't read → 0 classes extracted. That's a frontend-format gap belonging to the React-Flow migration, not this import refactor.
  • NN failures were a harness bug (file path passed as the NN name); the two project templates have spaces in their names, which export correctly rejects.

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.

1 participant