Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions ci/tests/200-xslt-step-xxe/channels/01-xslt-xxe/channel.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<channel version="4.6.0">
<id>5ec00001-0000-4000-8000-00000000c1a5</id>
<nextMetaDataId>2</nextMetaDataId>
<name>XSLT Step XXE</name>
<description></description>
<revision>1</revision>
<sourceConnector version="4.6.0">
<metaDataId>0</metaDataId>
<name>sourceConnector</name>
<properties class="com.mirth.connect.connectors.vm.VmReceiverProperties" version="4.6.0">
<pluginProperties/>
<sourceConnectorProperties version="4.6.0">
<responseVariable>None</responseVariable>
<respondAfterProcessing>true</respondAfterProcessing>
<processBatch>false</processBatch>
<firstResponse>false</firstResponse>
<processingThreads>1</processingThreads>
<resourceIds class="linked-hash-map">
<entry>
<string>Default Resource</string>
<string>[Default Resource]</string>
</entry>
</resourceIds>
<queueBufferSize>1000</queueBufferSize>
</sourceConnectorProperties>
</properties>
<transformer version="4.6.0">
<elements>
<com.mirth.connect.plugins.xsltstep.XsltStep version="4.6.0">
<name>XSLT XXE</name>
<sequenceNumber>0</sequenceNumber>
<enabled>true</enabled>
<sourceXml>connectorMessage.getRawData()</sourceXml>
<resultVariable>xsltResult</resultVariable>
<template>&apos;&lt;xsl:stylesheet version=&quot;1.0&quot; xmlns:xsl=&quot;http://www.w3.org/1999/XSL/Transform&quot;&gt;&lt;xsl:output method=&quot;text&quot;/&gt;&lt;xsl:template match=&quot;/&quot;&gt;&lt;xsl:value-of select=&quot;.&quot;/&gt;&lt;/xsl:template&gt;&lt;/xsl:stylesheet&gt;&apos;</template>
<useCustomFactory>false</useCustomFactory>
<customFactory></customFactory>
</com.mirth.connect.plugins.xsltstep.XsltStep>
</elements>
<inboundDataType>RAW</inboundDataType>
<outboundDataType>RAW</outboundDataType>
<inboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.6.0">
<batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.6.0">
<splitType>JavaScript</splitType>
<batchScript></batchScript>
</batchProperties>
</inboundProperties>
<outboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.6.0">
<batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.6.0">
<splitType>JavaScript</splitType>
<batchScript></batchScript>
</batchProperties>
</outboundProperties>
</transformer>
<filter version="4.6.0">
<elements/>
</filter>
<transportName>Channel Reader</transportName>
<mode>SOURCE</mode>
<enabled>true</enabled>
<waitForPrevious>true</waitForPrevious>
</sourceConnector>
<destinationConnectors>
<connector version="4.6.0">
<metaDataId>1</metaDataId>
<name>Destination 1</name>
<properties class="com.mirth.connect.connectors.vm.VmDispatcherProperties" version="4.6.0">
<pluginProperties/>
<destinationConnectorProperties version="4.6.0">
<queueEnabled>false</queueEnabled>
<sendFirst>false</sendFirst>
<retryIntervalMillis>10000</retryIntervalMillis>
<regenerateTemplate>false</regenerateTemplate>
<retryCount>0</retryCount>
<rotate>false</rotate>
<includeFilterTransformer>false</includeFilterTransformer>
<threadCount>1</threadCount>
<threadAssignmentVariable></threadAssignmentVariable>
<validateResponse>false</validateResponse>
<resourceIds class="linked-hash-map">
<entry>
<string>Default Resource</string>
<string>[Default Resource]</string>
</entry>
</resourceIds>
<queueBufferSize>1000</queueBufferSize>
<reattachAttachments>true</reattachAttachments>
</destinationConnectorProperties>
<channelId>none</channelId>
<channelTemplate>${message.encodedData}</channelTemplate>
<mapVariables/>
</properties>
<transformer version="4.6.0">
<elements/>
<inboundDataType>RAW</inboundDataType>
<outboundDataType>RAW</outboundDataType>
<inboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.6.0">
<batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.6.0">
<splitType>JavaScript</splitType>
<batchScript></batchScript>
</batchProperties>
</inboundProperties>
<outboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.6.0">
<batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.6.0">
<splitType>JavaScript</splitType>
<batchScript></batchScript>
</batchProperties>
</outboundProperties>
</transformer>
<responseTransformer version="4.6.0">
<elements/>
<inboundDataType>RAW</inboundDataType>
<outboundDataType>RAW</outboundDataType>
<inboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.6.0">
<batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.6.0">
<splitType>JavaScript</splitType>
<batchScript></batchScript>
</batchProperties>
</inboundProperties>
<outboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.6.0">
<batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.6.0">
<splitType>JavaScript</splitType>
<batchScript></batchScript>
</batchProperties>
</outboundProperties>
</responseTransformer>
<filter version="4.6.0">
<elements/>
</filter>
<transportName>Channel Writer</transportName>
<mode>DESTINATION</mode>
<enabled>true</enabled>
<waitForPrevious>true</waitForPrevious>
</connector>
</destinationConnectors>
<preprocessingScript>// Modify the message variable below to pre process data
return message;</preprocessingScript>
<postprocessingScript>// This script executes once after a message has been processed
// Responses returned from here will be stored as &quot;Postprocessor&quot; in the response map
return;</postprocessingScript>
<deployScript>// This script executes once when the channel is deployed
// You only have access to the globalMap and globalChannelMap here to persist data
return;</deployScript>
<undeployScript>// This script executes once when the channel is undeployed
// You only have access to the globalMap and globalChannelMap here to persist data
return;</undeployScript>
<properties version="4.6.0">
<clearGlobalChannelMap>true</clearGlobalChannelMap>
<messageStorageMode>DEVELOPMENT</messageStorageMode>
<encryptData>false</encryptData>
<encryptAttachments>false</encryptAttachments>
<encryptCustomMetaData>false</encryptCustomMetaData>
<removeContentOnCompletion>false</removeContentOnCompletion>
<removeOnlyFilteredOnCompletion>false</removeOnlyFilteredOnCompletion>
<removeAttachmentsOnCompletion>false</removeAttachmentsOnCompletion>
<initialState>STARTED</initialState>
<storeAttachments>true</storeAttachments>
<metaDataColumns>
<metaDataColumn>
<name>SOURCE</name>
<type>STRING</type>
<mappingName>mirth_source</mappingName>
</metaDataColumn>
<metaDataColumn>
<name>TYPE</name>
<type>STRING</type>
<mappingName>mirth_type</mappingName>
</metaDataColumn>
</metaDataColumns>
<attachmentProperties version="4.6.0">
<type>None</type>
<properties/>
</attachmentProperties>
<resourceIds class="linked-hash-map">
<entry>
<string>Default Resource</string>
<string>[Default Resource]</string>
</entry>
</resourceIds>
</properties>
<exportData>
<metadata>
<enabled>true</enabled>
<lastModified>
<time>1775955953073</time>
<timezone>America/Chicago</timezone>
</lastModified>
<pruningSettings>
<archiveEnabled>true</archiveEnabled>
<pruneErroredMessages>false</pruneErroredMessages>
</pruningSettings>
<userId>1</userId>
</metadata>
</exportData>
</channel>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0"?><!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/hostname">]><r>&x;</r>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ERROR
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0"?><r>hello</r>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TRANSFORMED
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.apache.logging.log4j.Logger;

import com.mirth.connect.client.core.api.MirthApiException;
import com.mirth.connect.model.DriverInfo;
import com.mirth.connect.server.api.MirthServlet;
import com.mirth.connect.server.controllers.ContextFactoryController;
import com.mirth.connect.server.controllers.ControllerFactory;
Expand All @@ -52,6 +53,13 @@ public DatabaseConnectorServlet(@Context HttpServletRequest request, @Context Se

@Override
public SortedSet<Table> getTables(String channelId, String channelName, String driver, String url, String username, String password, Set<String> tableNamePatterns, String selectLimit, Set<String> resourceIds) {
// selectLimit is a driver-specific, server-owned metadata-probe template, not a caller input.
// The caller-supplied value is ignored and the template is resolved from the built-in driver
// list keyed by the JDBC driver class; anything unrecognised uses the safe
// DatabaseMetaData.getColumns() path. This removes the SQL-injection surface (CVE-2026-82583)
// without depending on any API-writable configuration.
String resolvedSelectLimit = resolveSelectLimit(driver);

CustomDriver customDriver = null;
Connection connection = null;
try {
Expand Down Expand Up @@ -150,22 +158,24 @@ public SortedSet<Table> getTables(String channelId, String channelName, String d
// then we'll define to the generic method of getting column information, but
// this could be extremely slow
List<Column> columnList = new ArrayList<Column>();
if (StringUtils.isEmpty(selectLimit)) {
if (StringUtils.isEmpty(resolvedSelectLimit)) {
logger.debug("No select limit is defined, using generic method");
rs = dbMetaData.getColumns(null, null, tableName, null);
// Scope to the discovered schema (may be null) so a same-named table in
// another schema does not leak its columns into the result.
rs = dbMetaData.getColumns(null, schema, tableName, null);

// retrieve all relevant column information
for (int i = 0; rs.next(); i++) {
Column column = new Column(rs.getString("COLUMN_NAME"), rs.getString("TYPE_NAME"), rs.getInt("COLUMN_SIZE"));
columnList.add(column);
}
} else {
logger.debug("Select limit is defined, using specific select query : '" + selectLimit + "'");
logger.debug("Select limit is defined, using specific select query : '" + resolvedSelectLimit + "'");

// replace the '?' with the appropriate schema.table name, and use ResultSetMetaData to
// retrieve column information
final String schemaTableName = StringUtils.isNotEmpty(schema) ? "\"" + schema + "\".\"" + tableName + "\"" : "\"" + tableName + "\"";
final String queryString = selectLimit.trim().replaceAll("\\?", Matcher.quoteReplacement(schemaTableName));
// replace the '?' with the appropriate schema.table name, and use ResultSetMetaData to
// retrieve column information
final String schemaTableName = quoteSchemaTable(schema, tableName);
final String queryString = resolvedSelectLimit.trim().replaceAll("\\?", Matcher.quoteReplacement(schemaTableName));
Statement statement = connection.createStatement();
try {
rs = statement.executeQuery(queryString);
Expand All @@ -192,7 +202,7 @@ public SortedSet<Table> getTables(String channelId, String channelName, String d
columnList = new ArrayList<Column>();

logger.debug("Using fallback method for retrieving columns");
backupRs = dbMetaData.getColumns(null, null, tableName.replace("/", "//"), null);
backupRs = dbMetaData.getColumns(null, schema, tableName.replace("/", "//"), null);

// retrieve all relevant column information
while (backupRs.next()) {
Expand Down Expand Up @@ -229,6 +239,50 @@ public SortedSet<Table> getTables(String channelId, String channelName, String d
}
}

/**
* Resolves the driver-specific metadata-probe query from the built-in driver definitions, keyed
* by the JDBC driver class (including known alternative class names). Returns {@code ""} - the
* safe {@link DatabaseMetaData#getColumns} path - for anything not built in.
* <p>
* The caller-supplied {@code selectLimit} query parameter is deliberately ignored: it is executed
* as SQL (CVE-2026-82583), and the metadata dialog only ever sends the driver's own template
* anyway. This method consults only {@link DriverInfo#getDefaultDrivers()}, never the
* API-writable configured driver list, so a caller cannot introduce an arbitrary query even by
* first writing it to the driver configuration. Package-private and static so it is unit-testable
* without a servlet instance or a live server.
*/
static String resolveSelectLimit(String driver) {
if (StringUtils.isBlank(driver)) {
return "";
}

for (DriverInfo driverInfo : DriverInfo.getDefaultDrivers()) {
if (driver.equals(driverInfo.getClassName())
|| (driverInfo.getAlternativeClassNames() != null && driverInfo.getAlternativeClassNames().contains(driver))) {
return StringUtils.defaultString(driverInfo.getSelectLimit());
}
}

return "";
}

/**
* Builds the {@code "schema"."table"} (or {@code "table"}) identifier that replaces the {@code ?}
* placeholder in the metadata-probe query. The schema and table names come from the database's own
* metadata ({@link DatabaseMetaData#getSchemas}/{@link DatabaseMetaData#getTables}), but a name may
* still contain a double quote; embedding it verbatim would break out of the quoting and alter the
* query (a second-order SQL injection). Following the SQL standard, any embedded {@code "} is
* doubled so the value is always a single quoted identifier. Package-private and static so it is
* unit-testable without a live database.
*/
static String quoteSchemaTable(String schema, String tableName) {
String quotedTable = "\"" + StringUtils.defaultString(tableName).replace("\"", "\"\"") + "\"";
if (StringUtils.isNotEmpty(schema)) {
return "\"" + schema.replace("\"", "\"\"") + "\"." + quotedTable;
}
return quotedTable;
}

/**
* Translate the given pattern expression so that it can be used properly for searching tables
* in the database. Multiple table name patterns are delimited by comma (,)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public SortedSet<Table> getTables(// @formatter:off
@Param("username") @Parameter(description = "The username to authenticate with.") @DefaultValue("") @QueryParam("username") String username,
@Param(value = "password", excludeFromAudit = true) @Parameter(description = "The password to authenticate with.", schema = @Schema(format = "password")) @DefaultValue("") @QueryParam("password") String password,
@Param("tableNamePatterns") @Parameter(description = "If specified, filters by table name. Wildcards (* or %) are allowed.") @QueryParam("tableNamePattern") Set<String> tableNamePatterns,
@Param("selectLimit") @Parameter(description = "A simple query to use to retrieve database metadata information.", schema = @Schema(defaultValue = "SELECT * FROM ? LIMIT 1")) @DefaultValue("SELECT * FROM ? LIMIT 1") @QueryParam("selectLimit") String selectLimit,
@Param("selectLimit") @Parameter(description = "Deprecated and ignored: the metadata-probe query is resolved server-side from the driver class, never from this value, to prevent SQL injection (CVE-2026-82583). Retained only for wire/source compatibility.", schema = @Schema(defaultValue = "SELECT * FROM ? LIMIT 1")) @DefaultValue("SELECT * FROM ? LIMIT 1") @QueryParam("selectLimit") String selectLimit,
@Param("resourceIds") @Parameter(description = "Library resource IDs to use, if a custom driver is necessary.") @QueryParam("resourceId") Set<String> resourceIds) throws ClientException;
// @formatter:on)
}
Loading
Loading