001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020package org.apache.shiro.web.filter; 021 022import org.apache.shiro.lang.util.StringUtils; 023import org.apache.shiro.web.util.WebUtils; 024 025import javax.servlet.ServletRequest; 026import javax.servlet.ServletResponse; 027import javax.servlet.http.HttpServletRequest; 028import javax.servlet.http.HttpServletResponse; 029import java.util.Arrays; 030import java.util.Collections; 031import java.util.List; 032 033/** 034 * A request filter that blocks malicious requests. Invalid request will respond with a 400 response code. 035 * <p> 036 * This filter checks and blocks the request if the following characters are found in the request URI: 037 * <ul> 038 * <li>Semicolon - can be disabled by setting {@code blockSemicolon = false}</li> 039 * <li>Backslash - can be disabled by setting {@code blockBackslash = false}</li> 040 * <li>Non-ASCII characters - can be disabled by setting {@code blockNonAscii = false}, 041 * the ability to disable this check will be removed in future version.</li> 042 * <li>Path traversals - can be disabled by setting {@code blockTraversal = false}</li> 043 * </ul> 044 * 045 * @see <a href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/firewall/StrictHttpFirewall.html"> 046 * This class was inspired by Spring Security StrictHttpFirewall</a> 047 * @since 1.6 048 */ 049public class InvalidRequestFilter extends AccessControlFilter { 050 public enum PathTraversalBlockMode { 051 STRICT, 052 NORMAL, 053 NO_BLOCK; 054 } 055 056 private static final List<String> SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B")); 057 058 private static final List<String> BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C")); 059 060 private static final List<String> FORWARDSLASH = Collections.unmodifiableList(Arrays.asList("%2f", "%2F")); 061 062 private static final List<String> PERIOD = Collections.unmodifiableList(Arrays.asList("%2e", "%2E")); 063 064 private boolean blockSemicolon = true; 065 066 private boolean blockBackslash = !WebUtils.isAllowBackslash(); 067 068 private boolean blockNonAscii = true; 069 070 private PathTraversalBlockMode pathTraversalBlockMode = PathTraversalBlockMode.NORMAL; 071 072 @Override 073 protected boolean isAccessAllowed(ServletRequest req, ServletResponse response, Object mappedValue) throws Exception { 074 HttpServletRequest request = WebUtils.toHttp(req); 075 // check the original and decoded values 076 077 // user request string (not decoded) 078 return isValid(request.getRequestURI()) 079 // decoded servlet part 080 && isValid(request.getServletPath()) 081 // decoded path info (maybe null) 082 && isValid(request.getPathInfo()); 083 } 084 085 private boolean isValid(String uri) { 086 return !StringUtils.hasText(uri) 087 || (!containsSemicolon(uri) 088 && !containsBackslash(uri) 089 && !containsNonAsciiCharacters(uri)) 090 && !containsTraversal(uri); 091 } 092 093 @Override 094 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 095 WebUtils.toHttp(response).sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid request"); 096 return false; 097 } 098 099 private boolean containsSemicolon(String uri) { 100 if (isBlockSemicolon()) { 101 return SEMICOLON.stream().anyMatch(uri::contains); 102 } 103 return false; 104 } 105 106 private boolean containsBackslash(String uri) { 107 if (isBlockBackslash()) { 108 return BACKSLASH.stream().anyMatch(uri::contains); 109 } 110 return false; 111 } 112 113 private boolean containsNonAsciiCharacters(String uri) { 114 if (isBlockNonAscii()) { 115 return !containsOnlyPrintableAsciiCharacters(uri); 116 } 117 return false; 118 } 119 120 private static boolean containsOnlyPrintableAsciiCharacters(String uri) { 121 int length = uri.length(); 122 for (int i = 0; i < length; i++) { 123 char c = uri.charAt(i); 124 if (c < '\u0020' || c > '\u007e') { 125 return false; 126 } 127 } 128 return true; 129 } 130 131 private boolean containsTraversal(String uri) { 132 if (pathTraversalBlockMode == PathTraversalBlockMode.NORMAL) { 133 return !(isNormalized(uri)); 134 } 135 if (pathTraversalBlockMode == PathTraversalBlockMode.STRICT) { 136 return !(isNormalized(uri) 137 && PERIOD.stream().noneMatch(uri::contains) 138 && FORWARDSLASH.stream().noneMatch(uri::contains)); 139 } 140 return false; 141 } 142 143 /** 144 * Checks whether a path is normalized (doesn't contain path traversal sequences like 145 * "./", "/../" or "/.") 146 * 147 * @param path the path to test 148 * @return true if the path doesn't contain any path-traversal character sequences. 149 */ 150 private boolean isNormalized(String path) { 151 if (path == null) { 152 return true; 153 } 154 for (int i = path.length(); i > 0; ) { 155 int slashIndex = path.lastIndexOf('/', i - 1); 156 int gap = i - slashIndex; 157 if (gap == 2 && path.charAt(slashIndex + 1) == '.') { 158 // ".", "/./" or "/." 159 return false; 160 } 161 if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') { 162 return false; 163 } 164 i = slashIndex; 165 } 166 return true; 167 } 168 169 public boolean isBlockSemicolon() { 170 return blockSemicolon; 171 } 172 173 public void setBlockSemicolon(boolean blockSemicolon) { 174 this.blockSemicolon = blockSemicolon; 175 } 176 177 public boolean isBlockBackslash() { 178 return blockBackslash; 179 } 180 181 public void setBlockBackslash(boolean blockBackslash) { 182 this.blockBackslash = blockBackslash; 183 } 184 185 public boolean isBlockNonAscii() { 186 return blockNonAscii; 187 } 188 189 public void setBlockNonAscii(boolean blockNonAscii) { 190 this.blockNonAscii = blockNonAscii; 191 } 192 193 public PathTraversalBlockMode getPathTraversalBlockMode() { 194 return pathTraversalBlockMode; 195 } 196 197 public void setBlockPathTraversal(PathTraversalBlockMode mode) { 198 this.pathTraversalBlockMode = mode; 199 } 200 201 public boolean isBlockEncodedPeriod() { 202 return pathTraversalBlockMode == PathTraversalBlockMode.STRICT; 203 } 204 205 public void setBlockEncodedPeriod(boolean blockEncodedPeriod) { 206 setBlockPathTraversal(blockEncodedPeriod ? PathTraversalBlockMode.STRICT : PathTraversalBlockMode.NORMAL); 207 } 208 209 public boolean isBlockEncodedForwardSlash() { 210 return pathTraversalBlockMode == PathTraversalBlockMode.STRICT; 211 } 212 213 public void setBlockEncodedForwardSlash(boolean blockEncodedForwardSlash) { 214 setBlockPathTraversal(blockEncodedForwardSlash ? PathTraversalBlockMode.STRICT : PathTraversalBlockMode.NORMAL); 215 } 216 217 public boolean isBlockRewriteTraversal() { 218 return pathTraversalBlockMode == PathTraversalBlockMode.NORMAL; 219 } 220 221 public void setBlockRewriteTraversal(boolean blockRewriteTraversal) { 222 setBlockPathTraversal(blockRewriteTraversal ? PathTraversalBlockMode.NORMAL : PathTraversalBlockMode.NO_BLOCK); 223 } 224 225 /** 226 * @deprecated use {@link #getPathTraversalBlockMode()} instead 227 */ 228 @Deprecated 229 public boolean isBlockTraversal() { 230 return pathTraversalBlockMode != PathTraversalBlockMode.NO_BLOCK; 231 } 232 233 /** 234 * @deprecated Use {@link #setBlockPathTraversal(PathTraversalBlockMode)} 235 */ 236 @Deprecated 237 public void setBlockTraversal(boolean blockTraversal) { 238 this.pathTraversalBlockMode = blockTraversal ? PathTraversalBlockMode.NORMAL : PathTraversalBlockMode.NO_BLOCK; 239 } 240}