OpenMRS Core
Stored Velocity SSTI to RCE via ConceptReferenceRange
The ConceptReferenceRangeUtility.evaluateCriteria() method evaluates database-stored criteria strings as Apache Velocity templates without sandbox configuration. A user with the Manage Concepts privilege can store a malicious template expression that executes whenever an observation is validated against the affected concept.
Description
OpenMRS concept reference ranges carry a criteria string so that a range can apply conditionally, for example only to adults or only to a particular clinical state. The criteria string is stored in the database and evaluated as an Apache Velocity template. The engine that evaluates it is constructed per call and initialised with logging properties only.
ConceptReferenceRangeUtility.java:71-94 (2.8.4)
VelocityContext velocityContext = new VelocityContext();velocityContext.put("fn", this);velocityContext.put("obs", obs);velocityContext.put("patient", obs.getPerson());
VelocityEngine velocityEngine = new VelocityEngine();try { Properties props = new Properties(); props.put("runtime.log.logsystem.class", Log4JLogChute.class.getName()); props.put("runtime.log.logsystem.log4j.category", "velocity"); props.put("runtime.log.logsystem.log4j.logger", "velocity"); velocityEngine.init(props);}catch (Exception e) { throw new APIException("Failed to create the velocity engine: " + e.getMessage(), e);}
StringWriter writer = new StringWriter();String wrappedCriteria = "#set( $criteria = " + criteria + " )$criteria";
try { velocityEngine.evaluate(velocityContext, writer, ConceptReferenceRangeUtility.class.getName(), wrappedCriteria); return Boolean.parseBoolean(writer.toString());}The property set contains no introspector.uberspect.class entry, so Velocity falls back to its default UberspectImpl. That default performs no introspection filtering: the restrict lists that SecureUberspector applies, which are what keeps template expressions away from java.lang.Class, java.lang.Runtime, and java.lang.ProcessBuilder, are not in force. Every object placed in the context is therefore reachable for arbitrary method invocation, getClass() included, and from a Class handle a template expression can reach the rest of the JVM by reflection. The criteria string is concatenated into the template unmodified, so the stored value is the expression.
Reflection is the path to code execution, but it is not required for patient data to leave the system. The context objects alone expose the record directly:
| Context expression | Data reached | Category |
|---|---|---|
$patient.getGivenName(), $patient.getFamilyName() | Full legal name | PII |
$patient.getBirthdate(), $patient.getAge() | Date of birth, age | PII |
$patient.getPatientIdentifier() | Medical record number, national identifier | PII |
$obs.getValueNumeric() | The clinical value being recorded | PHI |
$fn.getLatestObs(...) | Any prior observation for the patient, including diagnoses, HIV status, and medication | PHI |
Evaluation is not triggered by the person who stored the criteria. It runs during observation validation, on whichever user or integration happens to record an observation against the affected concept next.
ObsValidator.java:302-306 (2.8.4)
ConceptReferenceRangeUtility referenceRangeUtility = new ConceptReferenceRangeUtility();List<ConceptReferenceRange> validRanges = new ArrayList<>();
for (ConceptReferenceRange referenceRange : referenceRanges) { if (referenceRangeUtility.evaluateCriteria(StringEscapeUtils.unescapeHtml4(referenceRange.getCriteria()), obs)) {The stored string is passed through StringEscapeUtils.unescapeHtml4() on the way to the evaluator, so HTML entity encoding applied on the way in is undone on the way out. Any input filtering that relies on the escaped form does not survive the round trip. In 2.8.5 the call site moved from ObsValidator to ConceptServiceImpl; the sink and its missing introspection policy are unchanged.
The criteria field is written through the Legacy UI concept form at /dictionary/concept.form, which is the standard interface for concept dictionary maintenance and is present on effectively every deployment. ConceptFormValidator checks the numeric bounds on a reference range and does not inspect the criteria content at all, so the string reaches the database as typed. The privilege required is Manage Concepts, which the OpenMRS Implementers' Guide assigns to the Data Assistant, Data Manager, and Informatics Manager roles. It is a content-management privilege held by data entry and supervisory staff, not an administrative one: no superuser, no System Developer.
We confirmed the full chain on OpenMRS Core 2.8.4 with webservices.rest 2.50.0 and legacyui 2.0.0. The test creates a role holding Manage Concepts and its supporting read privileges, creates a user in that role, stores a criteria payload as that user through the concept form, and then records an observation against the concept over the REST API as a separate user. Execution is confirmed by an outbound request from the server process to a local listener.
Proof-of-concept output (identifiers abbreviated)
[*] Phase 1: Creating low-privilege user (as admin)[+] Role created: PoC Clerk Privileges: Get Concept Datatypes, Manage Concepts, ...[+] User created: clerk<id>
[*] Phase 2: Planting criteria as 'clerk<id>'[+] Authenticated as 'clerk<id>'[+] Concept created: <uuid>
[*] Phase 3: Recording an observation LISTEN 0.0.0.0:9999 POST /openmrs/ws/rest/v1/obs
Code execution confirmed via HTTP callback FROM: concept dictionary editor (data clerk) TO: arbitrary code execution (Tomcat process)The payload stays in the concept_reference_range table and runs again on every subsequent observation against that concept, for every user and every API client, across restarts. Nothing in the observation record indicates that evaluation occurred.
OpenMRS did not add SecureUberspector, which is what we had suggested. They removed Velocity from this path entirely and replaced it with a Spring SpEL evaluation constrained by a SimpleEvaluationContext.
ConceptReferenceRangeUtility.java:74-76 (2.8.6)
private static final SimpleEvaluationContext EVAL_CONTEXT = SimpleEvaluationContext .forPropertyAccessors(new MapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess()) .withMethodResolvers(DataBindingMethodResolver.forInstanceMethodInvocation()).build();SimpleEvaluationContext is Spring's deliberately reduced context: no bean references, no type references, and therefore no route from an expression to Class or to arbitrary constructors. Property access is read-only and method invocation is restricted to instance methods on the objects actually bound into the evaluation root. The helper functions previously exposed as $fn were also moved out of the utility class into a separate CriteriaFunctions class, with a comment stating the reason: keeping them there means evaluateCriteria itself is not callable from inside an expression. The change shipped as commit 0801023b on 2026-04-08 and released in 2.8.6 on 2026-04-10, three weeks before the advisory was published.
Impact
- Persistent remote code execution as the application server process, triggered automatically on any subsequent observation validation against the affected concept.
- Privilege escalation from
Manage Concepts(a content-management privilege typically held by data entry staff) to arbitrary code execution. - Exposure of Protected Health Information through the template context, including patient identifiers, demographics, and clinical observations.
Mitigation
Update OpenMRS Core to 2.7.9 or 2.8.6. Until the update can be deployed, restrict the Manage Concepts privilege to trusted users and audit existing ConceptReferenceRange criteria entries in the database.
Defender's Checklist
Update to 2.7.9 or 2.8.6.
Affected: 2.7.0 through 2.7.8 and 2.8.0 through 2.8.5. The fix replaces the Velocity evaluation with a constrained SpEL context; there is no configuration flag that achieves the same thing on an unpatched version.
Audit stored criteria before and after updating.
Query
concept_reference_rangefor criteria strings and review anything that is not a plain comparison. A payload stored on a vulnerable version persists across the update; the new evaluator will reject it rather than run it, but the row is still there and tells you whether someone tried.Check who actually holds Manage Concepts.
Review role membership rather than assuming the privilege is administrative. It is frequently attached to data entry roles because concept dictionary maintenance is routine clinical work.
Look for outbound connections from the application process.
Code execution in this position runs inside the servlet container. Egress from the OpenMRS host to anything other than its known integrations is worth alerting on regardless of this specific finding.
Audit other template evaluation in your modules.
Any
VelocityEngineinitialised withoutintrospector.uberspect.classset toSecureUberspectorhas the same exposure if the template text comes from stored data. OpenMRS has been here before with the htmlformentry module (CVE-2020-24621).
Severity Reasoning
PR:H reflects that a named privilege is needed, but the CVSS label overstates how privileged the holder is. Manage Concepts is a content-management privilege that the OpenMRS Implementers' Guide assigns to data entry and supervisory staff, not to administrators. In deployment terms, the distance from that role to code execution as the server process is the finding.
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.
