WW-5646 Modernize path normalization in Include component (#1780)

* Modernize path normalization in Include component and handle edge cases

* Add tests for edge case dot-dot handling in Include path normalization

* Fix reversed path segment order in getContextRelativePath()

The for-each loop iterated the ArrayDeque head-to-tail (most
recently pushed first), which is the reverse of the old Stack's
insertion-order iteration. This caused rebuilt paths like
"car/view.jsp" to come out as "view.jsp/car".

Use descendingIterator() to restore the original oldest-first
ordering when rebuilding the flat path string.
This commit is contained in:
Arun
2026-07-17 15:48:08 +05:30
committed by GitHub
parent e5eb01abda
commit de599c91d3
2 changed files with 24 additions and 7 deletions
@@ -47,8 +47,10 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.StringTokenizer;
import java.util.Iterator;
/**
* <!-- START SNIPPET: javadoc -->
@@ -203,7 +205,7 @@ public class Include extends Component {
// .. is illegal in an absolute path according to the Servlet Spec and will cause
// known problems on Orion application servers.
if (returnValue.contains("..")) {
Stack<String> stack = new Stack<>();
Deque<String> segments = new ArrayDeque<>();
StringTokenizer pathParts = new StringTokenizer(returnValue.replace('\\', '/'), "/");
while (pathParts.hasMoreTokens()) {
@@ -211,20 +213,23 @@ public class Include extends Component {
if (!part.equals(".")) {
if (part.equals("..")) {
stack.pop();
if (!segments.isEmpty()) {
segments.pop();
}
} else {
stack.push(part);
segments.push(part);
}
}
}
StringBuilder flatPathBuffer = new StringBuilder();
for (int i = 0; i < stack.size(); i++) {
flatPathBuffer.append("/").append(stack.elementAt(i));
Iterator<String> it = segments.descendingIterator();
while (it.hasNext()) {
flatPathBuffer.append("/").append(it.next());
}
returnValue = flatPathBuffer.toString();
returnValue = flatPathBuffer.length() > 0 ? flatPathBuffer.toString() : "/";
}
return returnValue;
@@ -220,6 +220,18 @@ public class IncludeTagTest extends AbstractTagTest {
strutsBodyTagsAreReflectionEqual(tag, freshTag));
}
public void testGetContextRelativePathExcessDotDot() {
// Excess ".." segments beyond root should be silently clamped, not throw EmptyStackException
String result = Include.getContextRelativePath(request, "/../../../other/resource.jsp");
assertEquals("/other/resource.jsp", result);
}
public void testGetContextRelativePathAllDotDot() {
// All segments are ".." - should resolve to root
String result = Include.getContextRelativePath(request, "/a/../../..");
assertEquals("/", result);
}
public void testIncludeSetUseResponseEncodingTrue() throws Exception {
// TODO: If possible in future mock-test an actual content-includes with various encodings
// while setting the response encoding to match. Doesn't appear to be possible