概述
功能介绍
StrictHttpFirewall
是Spring 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
实现是DefaultHttpFirewall
,DefaultHttpFirewall
中对请求URL
的限制没有这一条,但是相应地安全性就下降了。这里需要提醒的是,对请求
URL
做标准化处理是一种比较"脆弱"的(fragile
)手段。建议即使请求会被拒绝,也尽量不要对其URL
做标准化。标准化的
URL
必须符合以下条件 :
在其requestURI
/contextPath
/servletPath
/pathInfo
中,必须不能包含以下字符串序列之一 :
["//","./","/…/","/."] -
如果请求
URL
中包含不可打印ASCII
字符则该请求会被拒绝。该规则不可关闭,因为对应风极高。
有关知识点 :
requestURI
:URL
中去除协议,主机名,端口之后其余的部分,contextPath
:requestURI
中对应webapp
的部分,servletPath
:requestURI
中对应识别Servlet
的部分- ,
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)
可以设置是否关闭该规则。缺省使用该规则。 -
如果请求
URL
在URL
编码后包含了%25
(URL
编码了的百分号%
),或者在URL
编码前包含了百分号%
则该请求会被拒绝。通过开关函数
setAllowUrlEncodedPercent(boolean)
可以设置是否关闭该规则。缺省使用该规则。 -
如果请求
URL
在URL
编码后包含了URL
编码的英文句号.
(%2e
或者%2E
)则该请求会被拒绝。通过开关函数
setAllowUrlEncodedPeriod(boolean)
可以设置是否关闭该规则。缺省使用该规则。
注意:这里提到的"
URL
编码后"对应英文是URL encoded
,"URL
编码前"指的是未执行URL
编码的原始URL
字符串,或者是"URL
编码后"的URL
经过解码URL decode
得到的URL
字符串(应该等于原始URL
字符串)。
继承关系
使用介绍
// 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;
}
}