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}