From 90751ff9a8a08acd046c6cf538034f58f2896ddf Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Wed, 20 May 2026 08:52:34 +0200 Subject: [PATCH] WW-5623 fix(core): HTML-encode form action in PostbackResult to prevent XSS PostbackResult.doExecute() embeds finalLocation into a
attribute via raw string concatenation. A double quote in the location breaks out of the attribute, enabling reflected XSS. The response Content-Type is text/html. Form field names and values elsewhere in the same class are properly URL-encoded via URLEncoder.encode(); the action attribute was not encoded at all. Wrap finalLocation with StringEscapeUtils.escapeHtml4() before embedding it in the form tag, consistent with the encoding approach used in DefaultActionProxy, Property, and TextProviderHelper. Adds three regression tests in PostbackResultTest: - testFormActionHtmlEscaping: XSS payload with attribute breakout - testFormActionEscapesAllHtmlSpecialChars: covers ", &, <, > - testFormActionCleanLocationUnchanged: regression for clean URLs --- .../apache/struts2/result/PostbackResult.java | 3 +- .../struts2/result/PostbackResultTest.java | 103 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/apache/struts2/result/PostbackResult.java b/core/src/main/java/org/apache/struts2/result/PostbackResult.java index 96015e8fdc..8c4056ca9c 100644 --- a/core/src/main/java/org/apache/struts2/result/PostbackResult.java +++ b/core/src/main/java/org/apache/struts2/result/PostbackResult.java @@ -21,6 +21,7 @@ import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.inject.Inject; +import org.apache.commons.text.StringEscapeUtils; import org.apache.struts2.dispatcher.mapper.ActionMapper; import org.apache.struts2.dispatcher.mapper.ActionMapping; @@ -101,7 +102,7 @@ protected void doExecute(String finalLocation, ActionInvocation invocation) thro // Render PrintWriter pw = new PrintWriter(response.getOutputStream()); - pw.write(""); + pw.write(""); writeFormElements(request, pw); writePrologueScript(pw); pw.write(""); diff --git a/core/src/test/java/org/apache/struts2/result/PostbackResultTest.java b/core/src/test/java/org/apache/struts2/result/PostbackResultTest.java index f019436f71..3ec70637f2 100644 --- a/core/src/test/java/org/apache/struts2/result/PostbackResultTest.java +++ b/core/src/test/java/org/apache/struts2/result/PostbackResultTest.java @@ -146,5 +146,108 @@ public void testPassingNullInvocation() throws Exception{ } } + /** + * WW-5623: Verify that HTML special characters in finalLocation are properly + * escaped in the rendered form action attribute. + */ + public void testFormActionHtmlEscaping() throws Exception { + ActionContext context = ActionContext.getContext(); + ValueStack stack = context.getValueStack(); + MockHttpServletRequest req = new MockHttpServletRequest(); + MockHttpServletResponse res = new MockHttpServletResponse(); + context.put(ServletActionContext.HTTP_REQUEST, req); + context.put(ServletActionContext.HTTP_RESPONSE, res); + + // Push an object with a malicious property onto the value stack + stack.push(new Object() { + public String getTargetUrl() { + return "/test\"onmouseover=\"alert(1)"; + } + }); + + PostbackResult result = new PostbackResult(); + result.setLocation("/redirect?url=${targetUrl}"); + result.setPrependServletContext(false); + + IMocksControl control = createControl(); + ActionInvocation mockInvocation = control.createMock(ActionInvocation.class); + expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes(); + expect(mockInvocation.getStack()).andReturn(stack).anyTimes(); + + control.replay(); + result.setActionMapper(container.getInstance(ActionMapper.class)); + + // Call doExecute directly with a malicious location containing all critical chars + result.doExecute("/test\"onmouseover=\"alert(1)\"¶m=