From f1bce134a23446c34f2670833140ac877155aebc Mon Sep 17 00:00:00 2001 From: ThisaraWeerakoon Date: Fri, 9 Oct 2026 15:50:07 +0530 Subject: [PATCH] Treat payloadFactory expression results as text unless already XML --- .../transform/PayloadFactoryMediator.java | 104 +++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/apache/synapse/mediators/transform/PayloadFactoryMediator.java b/modules/core/src/main/java/org/apache/synapse/mediators/transform/PayloadFactoryMediator.java index 36bd48c10..62f7af66a 100644 --- a/modules/core/src/main/java/org/apache/synapse/mediators/transform/PayloadFactoryMediator.java +++ b/modules/core/src/main/java/org/apache/synapse/mediators/transform/PayloadFactoryMediator.java @@ -26,7 +26,10 @@ import org.apache.synapse.mediators.AbstractMediator; import org.apache.synapse.util.xpath.SynapseXPath; +import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.StringReader; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -59,6 +62,17 @@ public class PayloadFactoryMediator extends AbstractMediator { */ private Pattern pattern = Pattern.compile("\\$(\\d)+"); + /** + * Parser used only to decide whether an argument value is already a standalone XML fragment. + * DTD and external entity support are off: the values tested here come from the message. + */ + private static final XMLInputFactory XML_PROBE_FACTORY = XMLInputFactory.newInstance(); + + static { + XML_PROBE_FACTORY.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); + XML_PROBE_FACTORY.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); + } + /** * Replaces the existing payload with a new payload as defined by the format and the argument list * @param synCtx the current message for mediation @@ -105,7 +119,13 @@ private void transformPayload(StringBuffer result, MessageContext synCtx) { while (matcher.find()) { String matchSeq = matcher.group(); int argIndex = Integer.parseInt(matchSeq.substring(1)); - matcher.appendReplacement(result, argValues[argIndex - 1].toString()); + if (argIndex < 1 || argIndex > argValues.length) { + handleException("Invalid argument index " + matchSeq + " in the payloadFactory " + + "format; " + argValues.length + " argument(s) are configured", synCtx); + } + // quoteReplacement keeps a literal '$' or '\' from being read as replacement syntax. + matcher.appendReplacement(result, + Matcher.quoteReplacement(argValues[argIndex - 1].toString())); } matcher.appendTail(result); } @@ -125,7 +145,8 @@ private Object[] getArgValues(MessageContext synCtx) { } else if (arg.getExpression() != null) { String value = arg.getExpression().stringValueOf(synCtx); if (value != null) { - argValues[i] = value; + // Escape the value unless it is already a well-formed XML fragment. + argValues[i] = isXML(value) ? value : escapeXmlText(value); } else { argValues[i] = ""; } @@ -137,6 +158,85 @@ private Object[] getArgValues(MessageContext synCtx) { return argValues; } + /** + * Reports whether the value is already a standalone, well-formed XML fragment. + * + * @param value argument value to test. + * @return true if the value parses on its own as XML. + */ + private static boolean isXML(String value) { + + String trimmed = value.trim(); + if (trimmed.length() < 4 || !trimmed.endsWith(">")) { + return false; + } + try { + XMLStreamReader reader = XML_PROBE_FACTORY.createXMLStreamReader(new StringReader(trimmed)); + while (reader.hasNext()) { + reader.next(); + } + reader.close(); + return true; + } catch (Exception e) { + // Broad on purpose: malformed markup can surface as an unchecked exception here. + return false; + } + } + + /** + * Escapes the characters that would otherwise be read as markup once the completed format is + * parsed, and drops the characters XML 1.0 does not permit at all. + * + * @param value argument value to escape. + * @return the value as XML character data. + */ + private static String escapeXmlText(String value) { + + StringBuilder escaped = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '&': + escaped.append("&"); + break; + case '<': + escaped.append("<"); + break; + case '>': + escaped.append(">"); + break; + case '"': + escaped.append("""); + break; + case '\'': + // Numeric rather than ', which predates XML in some consumers. + escaped.append("'"); + break; + default: + if (Character.isHighSurrogate(c) && i + 1 < value.length() + && Character.isLowSurrogate(value.charAt(i + 1))) { + // Keep a valid surrogate pair together so the filter below can't split it. + escaped.append(c).append(value.charAt(++i)); + } else if (isValidXmlCharacter(c)) { + escaped.append(c); + } + break; + } + } + return escaped.toString(); + } + + /** + * @param c character to test. + * @return true if XML 1.0 permits the character in a document. + */ + private static boolean isValidXmlCharacter(char c) { + + return c == 0x9 || c == 0xA || c == 0xD + || (c >= 0x20 && c <= 0xD7FF) + || (c >= 0xE000 && c <= 0xFFFD); + } + public String getFormat() { return format; }