mercure
Code Injection via eval() Sandbox Escape in Routing Rules
mercure's routing engine decides where incoming DICOM series go by evaluating administrator-defined rule expressions with Python's eval(), sandboxed only by an empty set of builtins. That sandbox is escaped with a standard object-traversal technique, so an administrator (or anyone who reaches an admin session) can run arbitrary code. A saved rule runs on every DICOM series the router evaluates.
MERCURE_UNSAFE_RULE_EVALUATION environment variable is set.CVEPendingGHSAPendingDescription
mercure is an open-source DICOM orchestration platform; we appreciate the maintainers' prompt, constructive response to this report. The routing engine decides where each incoming DICOM series goes by evaluating administrator-defined rule expressions.
Those expressions are evaluated with Python's eval(), sandboxed only by an empty builtins mapping:
app/common/rule_evaluation.py:49-71
safe_eval_cmds = {"float": float, "int": int, "str": str, "len": len, "bool": bool, "sum": sum, "round": round, "max": max, "min": min, "abs": abs, "pow": pow, "chr": chr, "ord": ord}
def eval_rule(rule: str, tags_dict: Dict[str, str]) -> Any: # ... rule = replace_tags(rule, tags_dict) # ... tags_obj = Tags(tags_dict) result = eval(rule, {"__builtins__": {}}, {**safe_eval_cmds, "tags": tags_obj}) # ... return result, tags_obj.tags_accessed()An empty __builtins__ removes direct access to built-in functions, but it does not stop object traversal: from any literal one can reach the base object class, enumerate its subclasses, and reach a class that exposes the real import machinery, regaining __import__ and arbitrary command execution. The vendor's own 0.4.1 release notes describe this as "trivially bypassed via MRO class traversal." We are not publishing a working payload.
The rule field reaches the evaluator without validation. HTML sanitization is applied to the tags field but not to the rule field:
app/webinterface/rules.py:269
testrule = form["rule"] # raw form input, no validationtestvalues = json.loads(form["testvalues"])result, attrs_accessed = rule_evaluation.eval_rule(testrule, testvalues)Two paths reach the sink. /rules/test evaluates the expression immediately and returns the result in the HTTP response, so command output and file contents come straight back to the caller. A rule saved via /rules/edit/{rule} persists in mercure.json and runs on every DICOM series the router evaluates, giving persistent execution.
Evaluation runs inside the web interface process (immediate path) or the router process (saved path), so execution reaches the shared filesystem, Redis, and the rest of the Docker network. The scope-change metric reflects that.
Command execution as the mercure user is confirmed by the returned output of an injected id:
Proof-of-concept output
uid=1000(mercure) gid=1000(mercure) groups=1000(mercure)Impact
- An attacker with an administrator session can execute arbitrary code on the mercure host. The
/rules/testendpoint returns the result inline, so files, environment variables, and credentials can be read directly through the web interface. - A routing rule saved by the attacker runs on every DICOM series the router evaluates, providing persistent code execution that fires automatically as imaging data flows through.
- Because evaluation runs inside the web interface or router process, the attacker reaches the shared filesystem, Redis, and other containers on the Docker network.
Mitigation
Upgrade to mercure 0.4.1, which evaluates rules with asteval and a cleared symbol table by default; the previous eval() path runs only when MERCURE_UNSAFE_RULE_EVALUATION is set, which should not be enabled. For any code in this shape, avoid eval() on user-supplied expressions: use a purpose-built safe evaluator (such as asteval) or a small parsed comparison DSL, and reject attribute access to dunder or private names.
Defender's Checklist
Upgrade to 0.4.1 or later
Deploy the patched release and confirm
MERCURE_UNSAFE_RULE_EVALUATIONis not set in any environment.Review saved routing rules
Audit
mercure.jsonfor rule expressions that contain attribute access or function calls beyond simple tag comparisons; these should not be present.Restrict administrator access
Limit who holds administrator credentials and confirm the default admin password has been changed.
Severity Reasoning
/rules/test) and persistent (saved rule) paths are reached over the network through the web interface.AC:LA single request carrying a crafted expression is sufficient.PR:HThe rule endpoints require an administrator session.UI:NThe immediate path returns the result directly; saved rules fire on automatic series evaluation without user interaction.S:CEvaluation in the web interface or router process reaches resources beyond that component: the shared filesystem, Redis, and other containers.C/I/A:HArbitrary code execution yields full read, modification, and denial over reachable data and services.References
How We Can Help
Who We Are
The security researchers behind this advisory.

Dr. rer. nat. Simon Weber
Senior Pentester & MedSec Researcher
I evaluate your SaMD with the same industry-defining security insight I contributed to the BAK MV for the revision of the B3S standard.
- PhD on Hospital Cybersecurity
- Critical vulnerabilities found in hospital systems
- Alumni of THB MedSec Research Group
- gematik Security Hero

Dipl.-Inf. Volker Schönefeld
Senior Application Security Expert
As a former CTO and developer turned pentester, I work alongside your team to uncover vulnerabilities and find solutions that fit your architecture.
- 20+ years as CTO, 50M+ app downloads
- Architected and secured large-scale IoT fleets
- Certified Web Exploitation Specialist
- gematik Security Hero
Looking for a Penetration Test?
Machine Spirits specializes in security assessments for medical devices and healthcare IT. From MDR penetration testing to C5 cloud compliance, we help MedTech companies meet regulatory requirements.
