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.util.StringUtils;
023import org.apache.shiro.web.util.WebUtils;
024
025import javax.servlet.ServletRequest;
026import javax.servlet.ServletResponse;
027import javax.servlet.http.HttpServletRequest;
028import java.util.Arrays;
029import java.util.Collections;
030import java.util.List;
031
032/**
033 * A request filter that blocks malicious requests. Invalid request will respond with a 400 response code.
034 * <p>
035 * This filter checks and blocks the request if the following characters are found in the request URI:
036 * <ul>
037 *     <li>Semicolon - can be disabled by setting {@code blockSemicolon = false}</li>
038 *     <li>Backslash - can be disabled by setting {@code blockBackslash = false}</li>
039 *     <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>
040 * </ul>
041 *
042 * @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>
043 * @since 1.6
044 */
045public class InvalidRequestFilter extends AccessControlFilter {
046
047    private static final List<String> SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
048
049    private static final List<String> BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
050
051    private boolean blockSemicolon = true;
052
053    private boolean blockBackslash = !Boolean.getBoolean(WebUtils.ALLOW_BACKSLASH);
054
055    private boolean blockNonAscii = true;
056
057    @Override
058    protected boolean isAccessAllowed(ServletRequest req, ServletResponse response, Object mappedValue) throws Exception {
059        HttpServletRequest request = WebUtils.toHttp(req);
060        // check the original and decoded values
061        return isValid(request.getRequestURI())      // user request string (not decoded)
062                && isValid(request.getServletPath()) // decoded servlet part
063                && isValid(request.getPathInfo());   // decoded path info (may be null)
064    }
065
066    private boolean isValid(String uri) {
067        return !StringUtils.hasText(uri)
068               || ( !containsSemicolon(uri)
069                 && !containsBackslash(uri)
070                 && !containsNonAsciiCharacters(uri));
071    }
072
073    @Override
074    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
075        WebUtils.toHttp(response).sendError(400, "Invalid request");
076        return false;
077    }
078
079    private boolean containsSemicolon(String uri) {
080        if (isBlockSemicolon()) {
081            return SEMICOLON.stream().anyMatch(uri::contains);
082        }
083        return false;
084    }
085
086    private boolean containsBackslash(String uri) {
087        if (isBlockBackslash()) {
088            return BACKSLASH.stream().anyMatch(uri::contains);
089        }
090        return false;
091    }
092
093    private boolean containsNonAsciiCharacters(String uri) {
094        if (isBlockNonAscii()) {
095            return !containsOnlyPrintableAsciiCharacters(uri);
096        }
097        return false;
098    }
099
100    private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
101        int length = uri.length();
102        for (int i = 0; i < length; i++) {
103            char c = uri.charAt(i);
104            if (c < '\u0020' || c > '\u007e') {
105                return false;
106            }
107        }
108        return true;
109    }
110
111    public boolean isBlockSemicolon() {
112        return blockSemicolon;
113    }
114
115    public void setBlockSemicolon(boolean blockSemicolon) {
116        this.blockSemicolon = blockSemicolon;
117    }
118
119    public boolean isBlockBackslash() {
120        return blockBackslash;
121    }
122
123    public void setBlockBackslash(boolean blockBackslash) {
124        this.blockBackslash = blockBackslash;
125    }
126
127    public boolean isBlockNonAscii() {
128        return blockNonAscii;
129    }
130
131    public void setBlockNonAscii(boolean blockNonAscii) {
132        this.blockNonAscii = blockNonAscii;
133    }
134}