This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit d18bcf61d18ac2aafd1060ad92384bec72e85c90 Author: opencode <[email protected]> AuthorDate: Mon Oct 19 15:00:28 2026 +0200 Trim and escape the Out tag plugin body fallback When the c:out value is null and no default attribute is given, the tag body provides the output. The reference tag handler buffers the body, trims it, and writes the result as text, escaping it when escapeXml is enabled. The tag plugin emitted the body inline instead, so the whitespace written around the body content was kept and the content of expressions within the body reached the response unescaped, whatever escapeXml said. The generated code now buffers the body through _jspx_page_context, like the handler does, and writes the trimmed string through the same helper that escapes the value attribute. The buffering is unwound in a finally block, as the container does for a buffered tag body, so a body that fails does not leave the output redirected to the discarded buffer. --- .../apache/jasper/tagplugins/jstl/core/Out.java | 24 +++++++++++++ .../jasper/tagplugins/jstl/core/TestOut.java | 41 ++++++++++++++++++++++ test/webapp/jstl/out-body.jsp | 38 ++++++++++++++++++++ webapps/docs/changelog.xml | 9 +++++ 4 files changed, 112 insertions(+) diff --git a/java/org/apache/jasper/tagplugins/jstl/core/Out.java b/java/org/apache/jasper/tagplugins/jstl/core/Out.java index 12cd8471b4..a5f0811149 100644 --- a/java/org/apache/jasper/tagplugins/jstl/core/Out.java +++ b/java/org/apache/jasper/tagplugins/jstl/core/Out.java @@ -52,6 +52,8 @@ public final class Out implements TagPlugin { String strDefName = ctxt.getTemporaryVariableName(); String strEscapeXmlName = ctxt.getTemporaryVariableName(); String strSkipBodyName = ctxt.getTemporaryVariableName(); + String strBodyContentName = ctxt.getTemporaryVariableName(); + String strBodyName = ctxt.getTemporaryVariableName(); // the value attribute is not mandatory; the tag body is a fallback. ctxt.generateImport("java.io.Reader"); @@ -90,7 +92,29 @@ public final class Out implements TagPlugin { "boolean " + strSkipBodyName + " = " + "org.apache.jasper.tagplugins.jstl.core.Out.output(out, " + strObjectName + ", " + strValName + ", " + strDefName + ", " + strEscapeXmlName + ");"); ctxt.generateJavaSource("if(!" + strSkipBodyName + ") {"); + // Neither the value nor the default produced output. As in the + // standard tag handler, capture the body content, then output it + // trimmed and escaped according to the escapeXml attribute. The + // capture is unwound in a finally block, so a failing body does + // not leave the output redirected, as the container does when a + // buffered tag body throws. + ctxt.generateJavaSource("jakarta.servlet.jsp.tagext.BodyContent " + + strBodyContentName + " = _jspx_page_context.pushBody();"); + ctxt.generateJavaSource("out = " + strBodyContentName + ";"); + ctxt.generateJavaSource("String " + strBodyName + ";"); + ctxt.generateJavaSource("try {"); ctxt.generateBody(); + ctxt.generateJavaSource("} finally {"); + ctxt.generateJavaSource(" " + strBodyName + " = " + + strBodyContentName + ".getString().trim();"); + ctxt.generateJavaSource(" out = _jspx_page_context.popBody();"); + ctxt.generateJavaSource("}"); + ctxt.generateJavaSource("if(" + strEscapeXmlName + ") {"); + ctxt.generateJavaSource(" out.write(org.apache.jasper.tagplugins.jstl.Util.escapeXml(" + + strBodyName + "));"); + ctxt.generateJavaSource("} else {"); + ctxt.generateJavaSource(" out.write(" + strBodyName + ");"); + ctxt.generateJavaSource("}"); ctxt.generateJavaSource("}"); } diff --git a/test/org/apache/jasper/tagplugins/jstl/core/TestOut.java b/test/org/apache/jasper/tagplugins/jstl/core/TestOut.java index f578928ba7..de68d3f547 100644 --- a/test/org/apache/jasper/tagplugins/jstl/core/TestOut.java +++ b/test/org/apache/jasper/tagplugins/jstl/core/TestOut.java @@ -56,4 +56,45 @@ public class TestOut extends AbstractTestTag { Assert.assertTrue(body.contains("OK - 4")); Assert.assertFalse(body.contains("FAIL")); } + + + /* + * When the value is null, the tag body is the fallback. Like the + * reference JSTL implementation, the body content is used as a string + * that is trimmed and then escaped according to escapeXml. + */ + @Test + public void testBodyFallback() throws Exception { + ByteChunk res = new ByteChunk(); + + int rc = getUrl("http://localhost:" + getPort() + + "/test/jstl/out-body.jsp", res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + // The whitespace surrounding the body content is removed + Assert.assertTrue(body, body.contains("TRIM=[spaced body]")); + // The body content is escaped, like the value attribute is + Assert.assertTrue(body, body.contains("ESC=[<x>]")); + // ... unless escaping is switched off + Assert.assertTrue(body, body.contains("RAW=[<x>]")); + // A body of only whitespace outputs nothing + Assert.assertTrue(body, body.contains("SPACE=[]")); + // A non-null value skips the body + Assert.assertTrue(body, body.contains("SKIP=[V]")); + // An empty body outputs nothing + Assert.assertTrue(body, body.contains("NONE=[]")); + // A nested tag body is buffered by both tags, and the outer one + // still trims the whole content + Assert.assertTrue(body, body.contains("NEST=[AB inner CD]")); + // A Reader is written without escaping and skips the body + Assert.assertTrue(body, body.contains("READER=[READOK]")); + Assert.assertFalse(body, body.contains("IGNORED")); + // The content of a failing body is not written + Assert.assertFalse(body, body.contains("lost")); + // ... and output continues after it + Assert.assertTrue(body, body.contains("LEAK=[]")); + Assert.assertTrue(body, body.contains("TAIL=[visible]")); + } } diff --git a/test/webapp/jstl/out-body.jsp b/test/webapp/jstl/out-body.jsp new file mode 100644 index 0000000000..7e25efda2a --- /dev/null +++ b/test/webapp/jstl/out-body.jsp @@ -0,0 +1,38 @@ +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%> +<%@ page session="true" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> +<% request.setAttribute("testReader", new java.io.StringReader("READOK")); %> +<p>TRIM=[<c:out value="${sessionScope.doesNotExist}"> + spaced body +</c:out>]</p> +<p>ESC=[<c:out value="${sessionScope.doesNotExist}">${'<x>'}</c:out>]</p> +<p>RAW=[<c:out value="${sessionScope.doesNotExist}" + escapeXml="false">${'<x>'}</c:out>]</p> +<p>SPACE=[<c:out value="${sessionScope.doesNotExist}"> </c:out>]</p> +<p>SKIP=[<c:out value="V">BODY</c:out>]</p> +<p>NONE=[<c:out value="${sessionScope.doesNotExist}"/>]</p> +<p>NEST=[<c:out value="${sessionScope.doesNotExist}">AB <c:out + value="${sessionScope.doesNotExist}"> inner </c:out> CD</c:out>]</p> +<p>READER=[<c:out value="${testReader}">IGNORED</c:out>]</p> +<p>LEAK=[<c:catch><c:out value="${sessionScope.doesNotExist}">lost<% + if (true) { + throw new IllegalStateException("body failed"); + } +%></c:out></c:catch>]</p> +<p>TAIL=[visible]</p> diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index ee206f6ff6..7c6798263a 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -450,6 +450,15 @@ which keeps the parameters of each tag to itself and never touches page scope attributes. (remm) </fix> + <fix> + The tag plugin for <code>c:out</code> now uses its body as the + fallback for a null value like the standard tag handler does: the + body content is captured, trimmed, then written with escaping + according to the <code>escapeXml</code> attribute, and the capture + is unwound if the body fails. The plugin previously wrote the body + inline, including its surrounding whitespace, and did not escape + the body content. (remm) + </fix> </changelog> </subsection> <subsection name="WebSocket"> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
