Spring Security Web : StrictHttpFirewall HTTP防火墙(严格模式)

本文介绍了Spring Security提供的防火墙实现,采用严格模式,遇可疑请求会抛异常拒绝。阐述了其决定是否拒绝请求的规则,如不在许可清单、请求未标准化、含不可打印字符等情况会被拒绝,部分规则可通过开关函数设置,还提及相关知识点及源代码版本。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

功能介绍

StrictHttpFirewallSpring Security Web提供的一个HTTP防火墙(对应概念模型接口HttpFirewall)实现,该实现采用了严格模式,遇到任何可疑的请求,会通过抛出异常RequestRejectedException拒绝该请求。StrictHttpFirewall也是Spring Security Web在安全过滤器代理FilterChainProxy内置缺省使用的HTTP防火墙机制。

该防火墙实现使用如下规则决定是否拒绝一个请求:

  • 不在HTTP method许可清单的请求会被拒绝。

    HTTP method许可清单通过方法setAllowedHttpMethods(Collection)设置。
    缺省被允许的HTTP method有 [DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT]。

  • 如果请求URL 不是标准化(normalize)的URL则该请求会被拒绝,以避免安全限制被绕过。

    StrictHttpFirewall中该规则不能被禁用,因为关掉该限制风险极高。一种想通过该规则又想尽量安全的替代方法,是在请求到达前对URL进行标准化处理。跟StrictHttpFirewall相对应的另外一个HttpFirewall实现是DefaultHttpFirewallDefaultHttpFirewall中对请求URL的限制没有这一条,但是相应地安全性就下降了。

    这里需要提醒的是,对请求URL做标准化处理是一种比较"脆弱"的(fragile)手段。建议即使请求会被拒绝,也尽量不要对其URL做标准化。

    标准化的URL必须符合以下条件 :
    在其requestURI/contextPath/servletPath/pathInfo中,必须不能包含以下字符串序列之一 :
    ["//","./","/…/","/."]

  • 如果请求URL 中包含不可打印ASCII字符则该请求会被拒绝。

    该规则不可关闭,因为对应风极高。

    有关知识点 :

    1. requestURI : URL中去除协议,主机名,端口之后其余的部分,
    2. contextPath : requestURI中对应webapp的部分,
    3. servletPathrequestURI中对应识别Servlet的部分
    4. ,pathInfo : requestURI中去掉contextPath,servletPath剩下的部分
  • 如果请求URL(无论是URL编码前还是URL编码后)包含了分号(;或者%3b或者%3B)则该请求会被拒绝。

    通过开关函数setAllowSemicolon(boolean) 可以设置是否关闭该规则。缺省使用该规则。

  • 如果请求URL(无论是URL编码前还是URL编码后)包含了斜杠(%2f或者%2F)则该请求会被拒绝。

    通过开关函数setAllowUrlEncodedSlash(boolean) 可以设置是否关闭该规则。缺省使用该规则。

  • 如果请求URL(无论是URL编码前还是URL编码后)包含了反斜杠(\或者%5c或者%5B)则该请求会被拒绝。

    通过开关函数setAllowBackSlash(boolean) 可以设置是否关闭该规则。缺省使用该规则。

  • 如果请求URLURL编码后包含了%25(URL编码了的百分号%),或者在URL编码前包含了百分号%则该请求会被拒绝。

    通过开关函数setAllowUrlEncodedPercent(boolean) 可以设置是否关闭该规则。缺省使用该规则。

  • 如果请求URLURL编码后包含了URL编码的英文句号.(%2e或者%2E)则该请求会被拒绝。

    通过开关函数setAllowUrlEncodedPeriod(boolean) 可以设置是否关闭该规则。缺省使用该规则。

注意:这里提到的"URL编码后"对应英文是URL encoded,"URL编码前"指的是未执行URL编码的原始URL字符串,或者是"URL编码后"的URL经过解码URL decode得到的URL字符串(应该等于原始URL字符串)。

继承关系

StrictHttpFirewall

使用介绍


// FilterChainProxy 代码片段
private void doFilterInternal(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {

		// 这里 firewall 缺省就是一个 StrictHttpFirewall 实例,
		// FilterChainProxy 允许外部设置使用指定的 firewall 实例
		// 这里对请求/响应对象进行防火墙增强,如果请求被检测到可疑会被拒绝,
		// 响应对象会被增加输出头部/cookie/redirect location值时的安全检查
		FirewalledRequest fwRequest = firewall
				.getFirewalledRequest((HttpServletRequest) request);
		HttpServletResponse fwResponse = firewall
				.getFirewalledResponse((HttpServletResponse) response);

		// 获取跟该请求匹配的所有安全过滤器
		List<Filter> filters = getFilters(fwRequest);

		if (filters == null || filters.size() == 0) {
			if (logger.isDebugEnabled()) {
				logger.debug(UrlUtils.buildRequestUrl(fwRequest)
						+ (filters == null ? " has no matching filters"
								: " has an empty filter list"));
			}

			fwRequest.reset();

			// 调用安全过滤器链 
			chain.doFilter(fwRequest, fwResponse);

			return;
		}

		// 构造并调用安全过滤器链  
		VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
		vfc.doFilter(fwRequest, fwResponse);
	}

源代码

源代码版本 : Spring Security Web 5.1.4.RELEASE

package org.springframework.security.web.firewall;

import org.springframework.http.HttpMethod;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;


public class StrictHttpFirewall implements HttpFirewall {
	/**
	 * Used to specify to #setAllowedHttpMethods(Collection) that any HTTP method should be allowed.
	 */
	private static final Set<String> ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(Collections.emptySet());

	private static final String ENCODED_PERCENT = "%25";

	private static final String PERCENT = "%";

	private static final List<String> FORBIDDEN_ENCODED_PERIOD = 
		Collections.unmodifiableList(Arrays.asList("%2e", "%2E"));

	private static final List<String> FORBIDDEN_SEMICOLON = 
		Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));

	private static final List<String> FORBIDDEN_FORWARDSLASH = 
		Collections.unmodifiableList(Arrays.asList("%2f", "%2F"));

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

	private Set<String> encodedUrlBlacklist = new HashSet<String>();

	private Set<String> decodedUrlBlacklist = new HashSet<String>();

	private Set<String> allowedHttpMethods = createDefaultAllowedHttpMethods();

	public StrictHttpFirewall() {
		urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
		urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
		urlBlacklistsAddAll(FORBIDDEN_BACKSLASH);

		this.encodedUrlBlacklist.add(ENCODED_PERCENT);
		this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD);
		this.decodedUrlBlacklist.add(PERCENT);
	}


	public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
		this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
	}

	public void setAllowedHttpMethods(Collection<String> allowedHttpMethods) {
		if (allowedHttpMethods == null) {
			throw new IllegalArgumentException("allowedHttpMethods cannot be null");
		}
		if (allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
			this.allowedHttpMethods = ALLOW_ANY_HTTP_METHOD;
		} else {
			this.allowedHttpMethods = new HashSet<>(allowedHttpMethods);
		}
	}

	
	public void setAllowSemicolon(boolean allowSemicolon) {
		if (allowSemicolon) {
			urlBlacklistsRemoveAll(FORBIDDEN_SEMICOLON);
		} else {
			urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
		}
	}


	public void setAllowUrlEncodedSlash(boolean allowUrlEncodedSlash) {
		if (allowUrlEncodedSlash) {
			urlBlacklistsRemoveAll(FORBIDDEN_FORWARDSLASH);
		} else {
			urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
		}
	}


	public void setAllowUrlEncodedPeriod(boolean allowUrlEncodedPeriod) {
		if (allowUrlEncodedPeriod) {
			this.encodedUrlBlacklist.removeAll(FORBIDDEN_ENCODED_PERIOD);
		} else {
			this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD);
		}
	}

	public void setAllowBackSlash(boolean allowBackSlash) {
		if (allowBackSlash) {
			urlBlacklistsRemoveAll(FORBIDDEN_BACKSLASH);
		} else {
			urlBlacklistsAddAll(FORBIDDEN_BACKSLASH);
		}
	}


	public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) {
		if (allowUrlEncodedPercent) {
			this.encodedUrlBlacklist.remove(ENCODED_PERCENT);
			this.decodedUrlBlacklist.remove(PERCENT);
		} else {
			this.encodedUrlBlacklist.add(ENCODED_PERCENT);
			this.decodedUrlBlacklist.add(PERCENT);
		}
	}

	private void urlBlacklistsAddAll(Collection<String> values) {
		this.encodedUrlBlacklist.addAll(values);
		this.decodedUrlBlacklist.addAll(values);
	}

	private void urlBlacklistsRemoveAll(Collection<String> values) {
		this.encodedUrlBlacklist.removeAll(values);
		this.decodedUrlBlacklist.removeAll(values);
	}

	@Override
	public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
		rejectForbiddenHttpMethod(request);
		rejectedBlacklistedUrls(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 FirewalledRequest(request) {
			@Override
			public void reset() {
			}
		};
	}

	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 whitelist " +
					this.allowedHttpMethods);
		}
	}

	private void rejectedBlacklistedUrls(HttpServletRequest request) {
		for (String forbidden : this.encodedUrlBlacklist) {
			if (encodedUrlContains(request, forbidden)) {
				throw new RequestRejectedException(
					"The request was rejected because the URL contained a potentially malicious String \"" 
					+ forbidden + "\"");
			}
		}
		for (String forbidden : this.decodedUrlBlacklist) {
			if (decodedUrlContains(request, forbidden)) {
				throw new RequestRejectedException(
					"The request was rejected because the URL contained a potentially malicious String \"" 
					+ forbidden + "\"");
			}
		}
	}

	@Override
	public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
		return new FirewalledResponse(response);
	}

	private static Set<String> createDefaultAllowedHttpMethods() {
		Set<String> result = new HashSet<>();
		result.add(HttpMethod.DELETE.name());
		result.add(HttpMethod.GET.name());
		result.add(HttpMethod.HEAD.name());
		result.add(HttpMethod.OPTIONS.name());
		result.add(HttpMethod.PATCH.name());
		result.add(HttpMethod.POST.name());
		result.add(HttpMethod.PUT.name());
		return result;
	}


    // 对请求 request URL 的四个部分做标准化检查 :
    // requestURI,  contextPath, servletPath, pathInfo
	private static boolean isNormalized(HttpServletRequest request) {
		if (!isNormalized(request.getRequestURI())) {
			return false;
		}
		if (!isNormalized(request.getContextPath())) {
			return false;
		}
		if (!isNormalized(request.getServletPath())) {
			return false;
		}
		if (!isNormalized(request.getPathInfo())) {
			return false;
		}
		return true;
	}

	private static boolean encodedUrlContains(HttpServletRequest request, String value) {
		if (valueContains(request.getContextPath(), value)) {
			return true;
		}
		return valueContains(request.getRequestURI(), value);
	}

	private static boolean decodedUrlContains(HttpServletRequest request, String value) {
		if (valueContains(request.getServletPath(), value)) {
			return true;
		}
		if (valueContains(request.getPathInfo(), value)) {
			return true;
		}
		return false;
	}

	private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
		int length = uri.length();
		for (int i = 0; i < length; i++) {
			char c = uri.charAt(i);
			if (c < '\u0020' || c > '\u007e') {
				return false;
			}
		}

		return true;
	}

	private static boolean valueContains(String value, String contains) {
		return value != null && value.contains(contains);
	}


    // 检测一个路径参数path是否是一个标准化了的路径 :
    // 1. 如果路径中包含连续两个反斜杠 "//" 则会被认为是非标准化的
    // 2. 如果路径中包含 "./" , "/../" , 或者 "/." , 则会被认为是非标准化的
	private static boolean isNormalized(String path) {
		if (path == null) {
			return true;
		}

		if (path.indexOf("//") > -1) {
			return false;
		}

		for (int j = path.length(); j > 0;) {
			int i = path.lastIndexOf('/', j - 1);
			int gap = j - i;

			if (gap == 2 && path.charAt(i + 1) == '.') {
				// ".", "/./" or "/."
				return false;
			} else if (gap == 3 && path.charAt(i + 1) == '.' && path.charAt(i + 2) == '.') {
				return false;
			}

			j = i;
		}

		return true;
	}

}

参考文章

<think>好的,我现在需要解决用户提到的Spring Security中的RequestRejectedException问题。用户提供的引用信息显示,这个错误通常是因为URL未规范化或被检测到潜在的安全风险。用户已经给出了一些解决方法,比如配置DefaultHttpFirewall,但可能还需要更详细的步骤或解释。 首先,我要确认这个异常的原因。根据引用[1]和[4],当URL未标准化或包含可疑字符时,Spring Security的严格防火墙会拦截请求。例如,双斜杠、路径遍历序列(如../)或特殊字符可能导致这个问题。 接下来,用户提到的解决方案是在启动类中配置DefaultHttpFirewall的Bean。引用[1]和[2]都提到了这一点,所以这应该是有效的。但需要详细说明如何正确配置,可能需要代码示例,并解释为什么这样做有效。 另外,引用[3]提到了SecurityContextHolderAwareRequestFilter,这可能在处理请求包装时相关,但用户的问题更直接与防火墙配置有关,所以可能不需要深入这部分,除非有其他问题。 用户可能需要了解其他可能的解决方案,比如放宽URL检查或自定义防火墙规则。例如,如果对接方无法修改URL,可能需要平台适配,如引用[4]所述。这时候可能需要自定义HttpFirewall来允许某些特定的非规范URL。 需要注意配置DefaultHttpFirewall可能带来的安全风险,比如允许不规范的URL可能增加攻击面。需要提醒用户权衡安全性和需求,并建议其他安全措施。 最后,生成相关问题时要围绕解决方案、替代方法、安全影响和异常的其他原因展开,以帮助用户全面了解问题。</think>### 解决Spring Security中RequestRejectedException错误的方法 当Spring Security检测到URL不规范或包含潜在危险字符时,会抛出`org.springframework.security.web.firewall.RequestRejectedException`。以下是分步解决方案: --- #### 方法1:配置`DefaultHttpFirewall`放宽URL检查 1. **在Spring Boot启动类中定义Bean** 通过替换默认的`StrictHttpFirewall`为更宽松的`DefaultHttpFirewall`,允许部分非规范化URL: ```java @Bean public HttpFirewall defaultHttpFirewall() { return new DefaultHttpFirewall(); } ``` **原理**:`DefaultHttpFirewall`对URL的检查较宽松,可避免因双斜杠(`//`)、路径遍历(如`../`)或特殊字符触发的拦截[^1][^2]。 2. **确保Security配置引用该Bean** 在`SecurityConfig`类中,需显式配置`httpFirewall`: ```java @Autowired private HttpFirewall httpFirewall; @Override protected void configure(HttpSecurity http) throws Exception { http .requestMatcher(request -> httpFirewall.getFirewalledRequest((HttpServletRequest) request)) // 其他安全配置 } ``` --- #### 方法2:自定义`HttpFirewall`规则(精细控制) 若需保留安全检查但调整特定规则,可继承`StrictHttpFirewall`并重写方法: ```java @Bean public HttpFirewall customFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); // 允许URL编码的斜杠(如%2F) firewall.setAllowUrlEncodedPercent(true); // 允许百分号 firewall.setAllowBackSlash(true); // 允许反斜杠 return firewall; } ``` **适用场景**:需兼容历史API或第三方系统非规范URL时[^4]。 --- #### 方法3:检查请求处理链的过滤器配置 若异常由过滤器链中的包装类引发(如`SecurityContextHolderAwareRequestFilter`),可尝试调整过滤器顺序或禁用非必要过滤器: ```java @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(customRequestFilter(), SecurityContextHolderAwareRequestFilter.class) // 其他配置 } ``` --- #### 安全风险提示 放宽URL检查可能导致路径注入、目录遍历等漏洞。建议: - 仅在生产环境临时使用`DefaultHttpFirewall` - 配合输入验证和输出编码 - 监控日志中的异常请求模式[^4] ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值