getPurgedProperties() {
purgedProperties.put("handleRepetitions", handleRepetitions);
purgedProperties.put("handleSubcomponents", handleSubcomponents);
purgedProperties.put("useStrictParser", useStrictParser);
+ purgedProperties.put("allowUnknownMessageTypes", allowUnknownMessageTypes);
purgedProperties.put("stripNamespaces", stripNamespaces);
purgedProperties.put("convertLineBreaks", convertLineBreaks);
return purgedProperties;
diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/ER7SerializerXxeTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/ER7SerializerXxeTest.java
new file mode 100644
index 0000000000..eb95c23c81
--- /dev/null
+++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/ER7SerializerXxeTest.java
@@ -0,0 +1,87 @@
+package com.mirth.connect.plugins.datatypes.hl7v2;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.mirth.connect.donkey.model.message.MessageSerializerException;
+import com.mirth.connect.model.datatype.SerializerProperties;
+
+/**
+ * XXE coverage for {@link ER7Serializer#fromXML(String)}. The lenient handler and the strict (HAPI)
+ * parser are independent code paths; ci/tests/205-hl7-xxe covers the strict one end to end.
+ *
+ * Every payload declares a dummy entity holding " xmlns=". fromXML injects a namespace into the
+ * first '>'-delimited chunk of the message when that chunk does not already contain one, and with a
+ * DOCTYPE present the chunk is an entity declaration rather than the root element. The injection
+ * then corrupts the declaration into malformed XML and the parser rejects the message for being
+ * malformed instead of for its external entity, which makes an XXE test pass for the wrong reason.
+ * The dummy entity satisfies the check so the payload reaches the parser intact.
+ */
+public class ER7SerializerXxeTest {
+
+ private static File secret;
+
+ @BeforeClass
+ public static void writeSecret() throws Exception {
+ secret = File.createTempFile("xxe", ".txt");
+ secret.deleteOnExit();
+ FileUtils.write(secret, "canary", UTF_8);
+ }
+
+ @Test
+ public void lenientParserDoesNotResolveExternalEntity() {
+ assertExternalEntityNotResolved(false, false);
+ }
+
+ @Test
+ public void strictParserDoesNotResolveExternalEntity() {
+ assertExternalEntityNotResolved(true, false);
+ }
+
+ @Test
+ public void strictValidatingParserDoesNotResolveExternalEntity() {
+ assertExternalEntityNotResolved(true, true);
+ }
+
+ /** Guards the payload shape, so the tests above exercise a message the parser really reads. */
+ @Test
+ public void strictParserAcceptsDoctypeWithoutExternalEntity() throws Exception {
+ assertTrue(serializer(true, false).fromXML(message("", "asdf")).contains("asdf"));
+ }
+
+ /** Dropping the entity and rejecting the message are both fine; leaking the file is not. */
+ private static void assertExternalEntityNotResolved(boolean strictParser, boolean strictValidation) {
+ String xml = message("", "&xxe;");
+ String outcome;
+
+ try {
+ outcome = serializer(strictParser, strictValidation).fromXML(xml);
+ } catch (MessageSerializerException e) {
+ outcome = String.valueOf(e);
+ }
+
+ assertFalse(outcome, outcome.contains("canary"));
+ }
+
+ private static ER7Serializer serializer(boolean strictParser, boolean strictValidation) {
+ HL7v2DeserializationProperties properties = new HL7v2DeserializationProperties();
+ properties.setUseStrictParser(strictParser);
+ properties.setUseStrictValidation(strictValidation);
+ return new ER7Serializer(new SerializerProperties(new HL7v2SerializationProperties(), properties, null));
+ }
+
+ private static String message(String entityDeclaration, String sendingApplication) {
+ return "\n " + entityDeclaration + " ]>\n"
+ + "|^~\\&"
+ + "" + sendingApplication + "ACK"
+ + "12.4"
+ + "AA1";
+ }
+}
diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/Hl7v2CdataTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/Hl7v2CdataTest.java
new file mode 100644
index 0000000000..1b6fb0e22b
--- /dev/null
+++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/Hl7v2CdataTest.java
@@ -0,0 +1,86 @@
+package com.mirth.connect.plugins.datatypes.hl7v2;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.mirth.connect.model.datatype.SerializerProperties;
+
+/**
+ * A field written as CDATA must keep its contents.
+ *
+ *
+ * HAPI 2.6.0's {@code XMLUtils.parseDocument} builds its factory without coalescing, so the parser
+ * hands back CDATA_SECTION nodes, and {@code XMLParser.parsePrimitive} has only ever read text
+ * nodes. The content is then dropped with no error. HAPI 2.3 did not produce those nodes because it
+ * parsed through an {@code LSParser}, which is why this only appears after the upgrade.
+ *
+ *
+ * Both shapes are covered, a field that is entirely CDATA and one that mixes CDATA with ordinary
+ * text, on each strict path that reaches the parser: {@code fromXML} with validation off and on,
+ * and {@code toXML}, which only parses XML input when validation is also on.
+ */
+public class Hl7v2CdataTest {
+
+ private static final String PURE_CDATA = message("");
+ private static final String MIXED_CDATA = message("beforeafter");
+
+ @Test
+ public void fromXmlKeepsPureCdata() throws Exception {
+ assertKeeps("clinical", serializer(false).fromXML(PURE_CDATA));
+ }
+
+ @Test
+ public void fromXmlKeepsPureCdataWhenValidating() throws Exception {
+ assertKeeps("clinical", serializer(true).fromXML(PURE_CDATA));
+ }
+
+ @Test
+ public void fromXmlKeepsMixedCdata() throws Exception {
+ assertKeeps("beforeclinicalafter", serializer(false).fromXML(MIXED_CDATA));
+ }
+
+ @Test
+ public void fromXmlKeepsMixedCdataWhenValidating() throws Exception {
+ assertKeeps("beforeclinicalafter", serializer(true).fromXML(MIXED_CDATA));
+ }
+
+ /** toXML only parses XML input when strict validation is on; otherwise it passes it through. */
+ @Test
+ public void toXmlKeepsPureCdataWhenValidating() throws Exception {
+ assertKeeps("clinical", serializer(true).toXML(PURE_CDATA));
+ }
+
+ @Test
+ public void toXmlKeepsMixedCdataWhenValidating() throws Exception {
+ assertKeeps("beforeclinicalafter", serializer(true).toXML(MIXED_CDATA));
+ }
+
+ private static void assertKeeps(String expected, String actual) {
+ assertTrue("expected MSH-3 to still hold '" + expected + "', got: " + actual,
+ actual.contains(expected));
+ }
+
+ /** MSH-3 carries the payload, so a dropped field is visible in the sending application. */
+ private static String message(String sendingApplication) {
+ return "\n"
+ + ""
+ + "|^~\\&"
+ + "" + sendingApplication + ""
+ + "ADTA01ADT_A01"
+ + "MSGX2.4"
+ + "\n";
+ }
+
+ private static ER7Serializer serializer(boolean strictValidation) {
+ HL7v2SerializationProperties serialization = new HL7v2SerializationProperties();
+ serialization.setUseStrictParser(true);
+ serialization.setUseStrictValidation(strictValidation);
+
+ HL7v2DeserializationProperties deserialization = new HL7v2DeserializationProperties();
+ deserialization.setUseStrictParser(true);
+ deserialization.setUseStrictValidation(strictValidation);
+
+ return new ER7Serializer(new SerializerProperties(serialization, deserialization, null));
+ }
+}
diff --git a/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/Hl7v2UnknownMessageTypeTest.java b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/Hl7v2UnknownMessageTypeTest.java
new file mode 100644
index 0000000000..7002b1fc8b
--- /dev/null
+++ b/server/src/test/java/com/mirth/connect/plugins/datatypes/hl7v2/Hl7v2UnknownMessageTypeTest.java
@@ -0,0 +1,66 @@
+package com.mirth.connect.plugins.datatypes.hl7v2;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import com.mirth.connect.donkey.model.message.MessageSerializerException;
+import com.mirth.connect.model.datatype.SerializerProperties;
+
+/**
+ * A message type with no generated structure class parses into a HAPI {@code GenericMessage}.
+ * Through HAPI 2.3 the XML parser refused to encode one; 2.4 removed that refusal, so the same
+ * message now converts successfully and reaches the transformer as {@code },
+ * which no existing filter or transformer step is written against.
+ *
+ *
+ * The engine keeps rejecting it by default so existing channels are unaffected, and exposes the
+ * newer behaviour behind "Allow Unrecognized Message Types" for anyone who wants it.
+ */
+public class Hl7v2UnknownMessageTypeTest {
+
+ /** MSH-9 names a type no structure jar provides, so HAPI falls back to GenericMessage. */
+ private static final String UNKNOWN_TYPE =
+ "MSH|^~\\&|SENDAPP|SENDFAC|RECVAPP|RECVFAC|20260101120000||XYZ^Q01^XYZ_Q01|MSG00001|P|2.4\r";
+
+ /** The same message shape with a type HAPI does have a structure for. */
+ private static final String KNOWN_TYPE =
+ "MSH|^~\\&|SENDAPP|SENDFAC|RECVAPP|RECVFAC|20260101120000||ADT^A01^ADT_A01|MSG00002|P|2.4\r";
+
+ @Test
+ public void rejectsUnrecognizedMessageTypeByDefault() {
+ try {
+ serializer(false).toXML(UNKNOWN_TYPE);
+ fail("An unrecognized message type should be rejected unless the channel opts in");
+ } catch (MessageSerializerException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void encodesUnrecognizedMessageTypeWhenAllowed() throws Exception {
+ String xml = serializer(true).toXML(UNKNOWN_TYPE);
+
+ assertTrue("expected a generic message document, got: " + xml, xml.contains("GenericMessage"));
+ assertTrue("the original message type should survive the conversion: " + xml, xml.contains("XYZ"));
+ }
+
+ /** The option must not change anything for a type the parser does recognize. */
+ @Test
+ public void recognizedMessageTypeIsUnaffectedEitherWay() throws Exception {
+ String rejecting = serializer(false).toXML(KNOWN_TYPE);
+ String allowing = serializer(true).toXML(KNOWN_TYPE);
+
+ assertTrue("expected an ADT_A01 document, got: " + rejecting, rejecting.contains("ADT_A01"));
+ org.junit.Assert.assertEquals(rejecting, allowing);
+ }
+
+ private static ER7Serializer serializer(boolean allowUnknownMessageTypes) {
+ HL7v2SerializationProperties serialization = new HL7v2SerializationProperties();
+ serialization.setUseStrictParser(true);
+ serialization.setAllowUnknownMessageTypes(allowUnknownMessageTypes);
+
+ return new ER7Serializer(new SerializerProperties(serialization, new HL7v2DeserializationProperties(), null));
+ }
+}