源码分析异常错误The request was rejected because the URL contained a potentially malicio

2,691 阅读3分钟

项目上线之后经常会在日志当中看到如下错误:

org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String "//"
	at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlocklistedUrls(StrictHttpFirewall.java:456)
	at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:429)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:196)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)

这是什么原因呢,我们可以在spring的源码FilterChainProxy类中看到(FilterChainProxy的作用是管理一组过滤器,定义请求的安全验证和处理顺序,并提供基于拦截规则的安全功能。它是Spring Security框架中实现安全过滤器链的核心组件。)

public class FilterChainProxy extends GenericFilterBean {

    private static final Log logger = LogFactory.getLog(FilterChainProxy.class);

    private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");

    private List<SecurityFilterChain> filterChains;

    private FilterChainValidator filterChainValidator = new NullFilterChainValidator();

    private HttpFirewall firewall = new StrictHttpFirewall();

    private RequestRejectedHandler requestRejectedHandler = new DefaultRequestRejectedHandler();

    public FilterChainProxy() {
    }

    public FilterChainProxy(SecurityFilterChain chain) {
        this(Arrays.asList(chain));
    }

    public FilterChainProxy(List<SecurityFilterChain> filterChains) {
        this.filterChains = filterChains;
    }

    @Override
    public void afterPropertiesSet() {
        this.filterChainValidator.validate(this);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
        if (!clearContext) {
            doFilterInternal(request, response, chain);
            return;
        }
        try {
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            doFilterInternal(request, response, chain);
        } catch (RequestRejectedException ex) {
            this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
        } finally {
            SecurityContextHolder.clearContext();
            request.removeAttribute(FILTER_APPLIED);
        }
    }

    private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        //走到了这里,这里的firewall使用的是StrictHttpFirewall
        FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
        HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
        List<Filter> filters = getFilters(firewallRequest);
        if (filters == null || filters.size() == 0) {
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
            }
            firewallRequest.reset();
            chain.doFilter(firewallRequest, firewallResponse);
            return;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
        }
        VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
        virtualFilterChain.doFilter(firewallRequest, firewallResponse);
    }
}

代码执行到这里抛出了异常this.firewall.getFirewalledRequest((HttpServletRequest) request); 我们可以从上面代码看到firewall使用的是new StrictHttpFirewall(),让我们进入到StrictHttpFirewall的getFirewalledRequest看看:

public class StrictHttpFirewall implements HttpFirewall {
    @Override
	public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
		rejectForbiddenHttpMethod(request);
		rejectedBlocklistedUrls(request);
		rejectedUntrustedHosts(request);
		if (!isNormalized(request)) {
			throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
		}
		String requestUri = request.getRequestURI();
		if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
			throw new RequestRejectedException(
					"The requestURI was rejected because it can only contain printable ASCII characters.");
		}
		return new StrictFirewalledRequest(request);
	}

	private void rejectForbiddenHttpMethod(HttpServletRequest request) {
		if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
			return;
		}
		if (!this.allowedHttpMethods.contains(request.getMethod())) {
			throw new RequestRejectedException(
					"The request was rejected because the HTTP method \"" + request.getMethod()
							+ "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
		}
	}

	private void rejectedBlocklistedUrls(HttpServletRequest request) {
		for (String forbidden : this.encodedUrlBlocklist) {
		    //这个方法抛出了错误
			if (encodedUrlContains(request, forbidden)) {
				throw new RequestRejectedException(
						"The request was rejected because the URL contained a potentially malicious String \""
								+ forbidden + "\"");
			}
		}
		for (String forbidden : this.decodedUrlBlocklist) {
			if (decodedUrlContains(request, forbidden)) {
				throw new RequestRejectedException(
						"The request was rejected because the URL contained a potentially malicious String \""
								+ forbidden + "\"");
			}
		}
	}
    private static boolean encodedUrlContains(HttpServletRequest request, String value) {
        if (valueContains(request.getContextPath(), value)) {
            return true;
        }
        return valueContains(request.getRequestURI(), value);
    }
    private static boolean valueContains(String value, String contains) {
        return value != null && value.contains(contains);
    }
}

从以上代码我们找出来了原因,因为用户请求的路径中包含spring制定的违禁地址,所以抛出了RequestRejectedException异常

然而从FilterChainProxy中我们可以看到RequestRejectedException被requestRejectedHandler捕获了,而且处理器默认使用的是DefaultRequestRejectedHandler处理器,那DefaultRequestRejectedHandler是如何实现的呢?

public class DefaultRequestRejectedHandler implements RequestRejectedHandler {

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			RequestRejectedException requestRejectedException) throws IOException, ServletException {
		throw requestRejectedException;
	}

}

从DefaultRequestRejectedHandler源码中我们可以看到,他是直接把异常给抛出去了,所以会导致出现很多的RequestRejectedException错误日志

那么如何解决呢? 从上面的源码分析,我们可以看到问题出在firewall的校验url方法,我们可以替换为spring的DefaultHttpFirewall类,但是我个人不建议这么做。

还有一种方法,requestRejectedHandler的默认处理器DefaultRequestRejectedHandler直接抛出了异常导致程序出现异常信息,对于Spring安全版本5.4和更高版本,我们可以简单地创建一个类型为RequestRejectedHandler的bean,该bean将被注入Spring安全过滤器链中:

import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;
import org.springframework.security.web.firewall.RequestRejectedHandler;
 
@Bean
    public RequestRejectedHandler requestRejectedHandler() {
        // sends an error response with a configurable status code (default is 400 BAD_REQUEST)
        // we can pass a different value in the constructor
        return new HttpStatusRequestRejectedHandler();
    }

附下HttpStatusRequestRejectedHandler的源码:

public class HttpStatusRequestRejectedHandler implements RequestRejectedHandler {

	private static final Log logger = LogFactory.getLog(HttpStatusRequestRejectedHandler.class);

	private final int httpError;

	/**
	 * Constructs an instance which uses {@code 400} as response code.
	 */
	public HttpStatusRequestRejectedHandler() {
		this.httpError = HttpServletResponse.SC_BAD_REQUEST;
	}

	/**
	 * Constructs an instance which uses a configurable http code as response.
	 * @param httpError http status code to use
	 */
	public HttpStatusRequestRejectedHandler(int httpError) {
		this.httpError = httpError;
	}

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			RequestRejectedException requestRejectedException) throws IOException {
		logger.debug(LogMessage.format("Rejecting request due to: %s", requestRejectedException.getMessage()),
				requestRejectedException);
		response.sendError(this.httpError);
	}

}

如果您对我的问题分析感兴趣,不妨看看之前的文章