Skip to content

Commit c3ede3f

Browse files
committed
Adds improved path filter
1 parent 2eac0dc commit c3ede3f

File tree

2 files changed

+101
-1
lines changed

2 files changed

+101
-1
lines changed

web/src/main/java/org/apache/shiro/web/filter/InvalidRequestFilter.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
* <li>Semicolon - can be disabled by setting {@code blockSemicolon = false}</li>
3838
* <li>Backslash - can be disabled by setting {@code blockBackslash = false}</li>
3939
* <li>Non-ASCII characters - can be disabled by setting {@code blockNonAscii = false}, the ability to disable this check will be removed in future version.</li>
40+
* <li>Path traversals - can be disabled by setting {@code blockTraversal = false}</li>
4041
* </ul>
4142
*
4243
* @see <a href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/firewall/StrictHttpFirewall.html">This class was inspired by Spring Security StrictHttpFirewall</a>
@@ -48,12 +49,18 @@ public class InvalidRequestFilter extends AccessControlFilter {
4849

4950
private static final List<String> BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
5051

52+
private static final List<String> FORWARDSLASH = Collections.unmodifiableList(Arrays.asList("%2f", "%2F"));
53+
54+
private static final List<String> PERIOD = Collections.unmodifiableList(Arrays.asList("%2e", "%2E"));
55+
5156
private boolean blockSemicolon = true;
5257

5358
private boolean blockBackslash = !Boolean.getBoolean(WebUtils.ALLOW_BACKSLASH);
5459

5560
private boolean blockNonAscii = true;
5661

62+
private boolean blockTraversal = true;
63+
5764
@Override
5865
protected boolean isAccessAllowed(ServletRequest req, ServletResponse response, Object mappedValue) throws Exception {
5966
HttpServletRequest request = WebUtils.toHttp(req);
@@ -67,7 +74,8 @@ private boolean isValid(String uri) {
6774
return !StringUtils.hasText(uri)
6875
|| ( !containsSemicolon(uri)
6976
&& !containsBackslash(uri)
70-
&& !containsNonAsciiCharacters(uri));
77+
&& !containsNonAsciiCharacters(uri))
78+
&& !containsTraversal(uri);
7179
}
7280

7381
@Override
@@ -108,6 +116,39 @@ private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
108116
return true;
109117
}
110118

119+
private boolean containsTraversal(String uri) {
120+
if (isBlockTraversal()) {
121+
return !(isNormalized(uri)
122+
&& PERIOD.stream().noneMatch(uri::contains)
123+
&& FORWARDSLASH.stream().noneMatch(uri::contains));
124+
}
125+
return false;
126+
}
127+
128+
/**
129+
* Checks whether a path is normalized (doesn't contain path traversal sequences like
130+
* "./", "/../" or "/.")
131+
* @param path the path to test
132+
* @return true if the path doesn't contain any path-traversal character sequences.
133+
*/
134+
private boolean isNormalized(String path) {
135+
if (path == null) {
136+
return true;
137+
}
138+
for (int i = path.length(); i > 0;) {
139+
int slashIndex = path.lastIndexOf('/', i - 1);
140+
int gap = i - slashIndex;
141+
if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
142+
return false; // ".", "/./" or "/."
143+
}
144+
if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
145+
return false;
146+
}
147+
i = slashIndex;
148+
}
149+
return true;
150+
}
151+
111152
public boolean isBlockSemicolon() {
112153
return blockSemicolon;
113154
}
@@ -131,4 +172,12 @@ public boolean isBlockNonAscii() {
131172
public void setBlockNonAscii(boolean blockNonAscii) {
132173
this.blockNonAscii = blockNonAscii;
133174
}
175+
176+
public boolean isBlockTraversal() {
177+
return blockTraversal;
178+
}
179+
180+
public void setBlockTraversal(boolean blockTraversal) {
181+
this.blockTraversal = blockTraversal;
182+
}
134183
}

web/src/test/groovy/org/apache/shiro/web/filter/InvalidRequestFilterTest.groovy

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class InvalidRequestFilterTest {
3737
assertThat "filter.blockBackslash expected to be true", filter.isBlockBackslash()
3838
assertThat "filter.blockNonAscii expected to be true", filter.isBlockNonAscii()
3939
assertThat "filter.blockSemicolon expected to be true", filter.isBlockSemicolon()
40+
assertThat "filter.blockTraversal expected to be true", filter.isBlockTraversal()
4041
}
4142

4243
@Test
@@ -73,6 +74,31 @@ class InvalidRequestFilterTest {
7374
assertPathBlocked(filter, "/something", "/something", "/;")
7475
}
7576

77+
@Test
78+
void testBlocksTraversal() {
79+
InvalidRequestFilter filter = new InvalidRequestFilter()
80+
assertPathBlocked(filter, "/something/../")
81+
assertPathBlocked(filter, "/something/../bar")
82+
assertPathBlocked(filter, "/something/../bar/")
83+
assertPathBlocked(filter, "/something/%2e%2E/bar/")
84+
assertPathBlocked(filter, "/something/..")
85+
assertPathBlocked(filter, "/..")
86+
assertPathBlocked(filter, "..")
87+
assertPathBlocked(filter, "../")
88+
assertPathBlocked(filter, "%2E./")
89+
assertPathBlocked(filter, "%2F./")
90+
assertPathBlocked(filter, "/something/./")
91+
assertPathBlocked(filter, "/something/./bar")
92+
assertPathBlocked(filter, "/something/\u002e/bar")
93+
assertPathBlocked(filter, "/something/./bar/")
94+
assertPathBlocked(filter, "/something/%2e/bar/")
95+
assertPathBlocked(filter, "/something/%2f/bar/")
96+
assertPathBlocked(filter, "/something/.")
97+
assertPathBlocked(filter, "/.")
98+
assertPathBlocked(filter, "/something/../something/.")
99+
assertPathBlocked(filter, "/something/../something/.")
100+
}
101+
76102
@Test
77103
void testFilterAllowsBackslash() {
78104
InvalidRequestFilter filter = new InvalidRequestFilter()
@@ -120,6 +146,31 @@ class InvalidRequestFilterTest {
120146
assertPathAllowed(filter, "/something", "/something", "/;")
121147
}
122148

149+
@Test
150+
void testAllowTraversal() {
151+
InvalidRequestFilter filter = new InvalidRequestFilter()
152+
filter.setBlockTraversal(false)
153+
154+
assertPathAllowed(filter, "/something/../")
155+
assertPathAllowed(filter, "/something/../bar")
156+
assertPathAllowed(filter, "/something/../bar/")
157+
assertPathAllowed(filter, "/something/..")
158+
assertPathAllowed(filter, "/..")
159+
assertPathAllowed(filter, "..")
160+
assertPathAllowed(filter, "../")
161+
assertPathAllowed(filter, "%2E./")
162+
assertPathAllowed(filter, "%2F./")
163+
assertPathAllowed(filter, "/something/./")
164+
assertPathAllowed(filter, "/something/./bar")
165+
assertPathAllowed(filter, "/something/\u002e/bar")
166+
assertPathAllowed(filter, "/something\u002fbar")
167+
assertPathAllowed(filter, "/something/./bar/")
168+
assertPathAllowed(filter, "/something/%2e/bar/")
169+
assertPathAllowed(filter, "/something/%2f/bar/")
170+
assertPathAllowed(filter, "/something/.")
171+
assertPathAllowed(filter, "/.")
172+
assertPathAllowed(filter, "/something/../something/.")
173+
}
123174

124175
static void assertPathBlocked(InvalidRequestFilter filter, String requestUri, String servletPath = requestUri, String pathInfo = null) {
125176
assertThat "Expected path '${requestUri}', to be blocked", !filter.isAccessAllowed(mockRequest(requestUri, servletPath, pathInfo), null, null)

0 commit comments

Comments
 (0)