Java安全之Spring Security繞過總結
前言
bypass!bypass!bypass!
SpringSecurit使用
使用
@Configuration
@EnableWebSecurity //啓用Web安全功能
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests() // 開啓 HttpSecurity 配置
.antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL必須具備ADMIN角色
.antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 該模式需要ADMIN或USER角色
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 需ADMIN和DBA角色
.anyRequest().authenticated() // 用戶訪問其它URL都必須認證後訪問(登錄後訪問)
.and().formLogin().loginProcessingUrl("/login").permitAll() // 開啓表單登錄並配置登錄接口
.and().csrf().disable(); // 關閉csrf
return http.build();
}
方法 | 描述 |
---|---|
access(String) | 如果給定的SpEL表達式計算結果爲true,就允許訪問 |
anonymous() | 允許匿名用戶訪問 |
authenticated() | 允許認證過的用戶訪問 |
denyAll() | 無條件拒絕所有訪問 |
fullyAuthenticated() | 如果用戶是完整認證的話(不是通過Remember-me功能認證的),就允許訪問 |
hasAnyAuthority(String…) | 如果用戶具備給定權限中的某一個的話,就允許訪問 |
hasAnyRole(String…) | 如果用戶具備給定角色中的某一個的話,就允許訪問 |
hasAuthority(String) | 如果用戶具備給定權限的話,就允許訪問 |
hasIpAddress(String) | 如果請求來自給定IP地址的話,就允許訪問 |
hasRole(String) | 如果用戶具備給定角色的話,就允許訪問 |
not() | 對其他訪問方法的結果求反 |
permitAll() | 無條件允許訪問 |
rememberMe() | 如果用戶是通過Remember-me功能認證的,就允許訪問 |
也可以通過集成WebSecurityConfigurerAdapter
類的方式來configure()方法來制定Web安全的細節。
1、configure(WebSecurity):通過重載該方法,可配置Spring Security的Filter鏈。
2、configure(HttpSecurity):通過重載該方法,可配置如何通過攔截器保護請求。
Spring Security 支持的所有SpEL表達式如下:
安全表達式 | 計算結果 |
---|---|
authentication | 用戶認證對象 |
denyAll | 結果始終爲false |
hasAnyRole(list of roles) | 如果用戶被授權指定的任意權限,結果爲true |
hasRole(role) | 如果用戶被授予了指定的權限,結果 爲true |
hasIpAddress(IP Adress) | 用戶地址 |
isAnonymous() | 是否爲匿名用戶 |
isAuthenticated() | 不是匿名用戶 |
isFullyAuthenticated | 不是匿名也不是remember-me認證 |
isRemberMe() | remember-me認證 |
permitAll | 始終true |
principal | 用戶主要信息對象 |
configure(AuthenticationManagerBuilder):通過重載該方法,可配置user-detail(用戶詳細信息)服務。
方法 | 描述 |
---|---|
accountExpired(boolean) | 定義賬號是否已經過期 |
accountLocked(boolean) | 定義賬號是否已經鎖定 |
and() | 用來連接配置 |
authorities(GrantedAuthority…) | 授予某個用戶一項或多項權限 |
authorities(List) | 授予某個用戶一項或多項權限 |
authorities(String…) | 授予某個用戶一項或多項權限 |
credentialsExpired(boolean) | 定義憑證是否已經過期 |
disabled(boolean) | 定義賬號是否已被禁用 |
password(String) | 定義用戶的密碼 |
roles(String…) | 授予某個用戶一項或多項角色 |
用戶存儲方式
1、使用基於內存的用戶存儲:通過inMemoryAuthentication()方法,我們可以啓用、配置並任意填充基於內存的用戶存儲。並且,我們可以調用withUser()方法爲內存用戶存儲添加新的用戶,這個方法的參數是username。withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder,這個對象提供了多個進一步配置用戶的方法,包括設置用戶密碼的password()方法以及爲給定用戶授予一個或多個角色權限的roles()方法。需要注意的是,roles()方法是authorities()方法的簡寫形式。roles()方法所給定的值都會添加一個ROLE_前綴,並將其作爲權限授予給用戶。因此上訴代碼用戶具有的權限爲:ROLE_USER,ROLE_ADMIN。而藉助passwordEncoder()方法來指定一個密碼轉碼器(encoder),我們可以對用戶密碼進行加密存儲。
@Configuration
@EnableWebSecurity //啓用Web安全功能
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests() // 開啓 HttpSecurity 配置
.antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL必須具備ADMIN角色
.antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 該模式需要ADMIN或USER角色
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 需ADMIN和DBA角色
.anyRequest().authenticated() // 用戶訪問其它URL都必須認證後訪問(登錄後訪問)
.and().formLogin().loginProcessingUrl("/login").permitAll() // 開啓表單登錄並配置登錄接口
.and().csrf().disable(); // 關閉csrf
return http.build();
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("root").password("123").roles("ADMIN","DBA")
.and()
.withUser("admin").password("123").roles("ADMIN","USER")
.and()
.withUser("xxx").password("123").roles("USER");
}
}
2、基於數據庫表進行認證:用戶數據通常會存儲在關係型數據庫中,並通過JDBC進行訪問。爲了配置Spring Security使用以JDBC爲支撐的用戶存儲,我們可以使用jdbcAuthentication()方法,並配置他的DataSource,這樣的話,就能訪問關係型數據庫了。
3、基於LDAP進行認證:爲了讓Spring Security使用基於LDAP的認證,我們可以使用ldapAuthentication()方法。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 配置 user-detail 服務
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 基於 LDAP 配置認證
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}")
.passwordCompare()
.passwordAttribute("password")
.passwordEncoder(new BCryptPasswordEncoder());
}
}
使用遠程ldap
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}")
// 返回一個ContextSourceBuilder 對象
.contextSource()
// 指定遠程 LDAP 服務器 的 地址
.url("ldap://xxx.com:389/dc=xxx,dc=com");
}
}
ldapAuthentication():表示,基於LDAP的認證。
userSearchBase():爲查找用戶提供基礎查詢
userSearchFilter():提供過濾條件,用於搜索用戶。
groupSearchBase():爲查找組指定了基礎查詢。
groupSearchFilter():提供過濾條件,用於組。
passwordCompare():希望通過 密碼比對 進行認證。
passwordAttribute():指定 密碼 保存的屬性名字,默認:userPassword。
passwordEncoder():指定密碼轉換器。
hasRole 和 hasAuthority
http.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("admin")
.antMatchers("/user/**").hasAuthority("user")
.anyRequest().authenticated()
和
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
實際上這兩個的效果都是一樣的
antMatchers 配置認證繞過
package person.xu.vulEnv;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/test").access("hasRole('ADMIN')")
.antMatchers("/**").permitAll();
//.antMatchers("/**").access("anonymous");
// @formatter:on
return http.build();
}
// @formatter:off
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
// @formatter:on
}
繞過:http://127.0.0.1:8012/test/
mvcMatchers("/test").access("hasRole('ADMIN')")
或者使用 antMatchers("/test/**").access("hasRole('ADMIN')")
寫法防止認證繞過。
regexMatchers 配置認證繞過
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.regexMatchers("/test").access("hasRole('ADMIN')")
.antMatchers("/**").access("anonymous");
// @formatter:on
return http.build();
}
http://127.0.0.1:8012/test?
、http://127.0.0.1:8012/test/
Matchers沒使用類似/test.*
的方式,在傳入/test?
時候,正則會匹配不上,不會命中/test
的規則。
安全寫法
.regexMatchers("/test.*?").access("hasRole('ADMIN')")
useSuffixPatternMatch 繞過
低版本 的 spring-webmvc 及其相關組件,包括:
spring-webmvc <= 5.2.4.RELEASE
spring-framework <= 5.2.6.RELEASE
spring-boot-starter-parent <= 2.2.5.RELEASE
在代碼中定義的 useSuffixPatternMatch
配置默認值爲 true
,表示使用後綴匹配模式匹配路徑。
如 /path/abc
路由也會允許 /path/abcd.ef
、/path/abcde.f
等增加 .xxx
後綴形式的路徑匹配成功。
漏洞修復:
使用高版本的 spring-webmvc 能有效避免問題。
https://www.jianshu.com/p/e6655328b211
CVE-2022-22978
影響版本
Spring Security 5.5.x < 5.5.7
Spring Security 5.6.x < 5.6.4
漏洞分析
Spring在加載的時候會來到DelegatingFilterProxy
,DelegatingFilterProxy根據targetBeanName從Spring 容器中獲取被注入到Spring 容器的Filter實現類,在DelegatingFilterProxy配置時一般需要配置屬性targetBeanName。DelegatingFilterProxy就是一個對於servlet filter的代理,用這個類的好處主要是通過Spring容器來管理servlet filter的生命週期,
還有就是如果filter中需要一些Spring容器的實例,可以通過spring直接注入,
另外讀取一些配置文件這些便利的操作都可以通過Spring來配置實現。
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
//當Filter配置時如果沒有設置targentBeanName屬性,則直接根據Filter名稱來查找
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
//從Spring容器中獲取注入的Filter的實現類
this.delegate = initDelegate(wac);
}
}
}
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//從Spring 容器中獲取注入的Filter的實現類
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
//當Filter配置時如果沒有設置targentBeanName屬性,則直接根據Filter名稱來查找
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
//從Spring容器中獲取注入的Filter的實現類
this.delegate = initDelegate(wac);
}
}
}
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//從Spring 容器中獲取注入的Filter的實現類
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
從Spring 容器中獲取注入的Filter的實現類,然後調用org.springframework.web.filter.DelegatingFilterProxy#invokeDelegate
方法
來到org.springframework.security.web.FilterChainProxy#doFilterInternal
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
if (filters != null && filters.size() != 0) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> {
return "Securing " + requestLine(firewallRequest);
}));
}
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
} else {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> {
return "No security for " + requestLine(firewallRequest);
}));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
}
}
this.firewall
默認裝載的是StrictHttpFirewall
,而不是DefaultHttpFirewall
。反而DefaultHttpFirewall
的校驗沒那麼嚴格
FirewalledRequest
是封裝後的請求類,但實際上該類只是在HttpServletRequestWrapper
的基礎上增加了reset方法。當spring security過濾器鏈執行完畢時,由FilterChainProxy
負責調用該方法,以便重置全部或者部分屬性。
FirewalledResponse
是封裝後的響應類,該類主要重寫了sendRedirect、setHeader、addHeader以及addCookie四個方法,在每一個方法中都對其參數進行校驗,以確保參數中不含有\r和\n。
在FilterChainProxy屬性定義中,默認創建的HttpFirewall實例就是StrictHttpFirewall。
FilterChainProxy是在WebSecurity#performBuild方法中構建的,而WebSecurity實現了ApplicationContextAware接口,並實現了接口中的setApplicationContext方法,在該方法中,從spring容器中查找到HttpFirewall對並賦值給httpFirewall屬性。最終在performBuild方法中,將FilterChainProxy對象構建成功後,如果httpFirewall不爲空,就把httpFirewall配置給FilterChainProxy對象。
因此,如果spring容器中存在HttpFirewall實例,則最終使用spring容器提供的實例;如果不存在,則使用FilterChainProxy中默認定義的StrictHttpFirewall。
org.springframework.security.web.firewall.StrictHttpFirewall#getFirewalledRequest
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
this.rejectForbiddenHttpMethod(request);
this.rejectedBlocklistedUrls(request);
this.rejectedUntrustedHosts(request);
if (!isNormalized(request)) {
throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
} else {
String requestUri = request.getRequestURI();
if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
throw new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters.");
} else {
return new StrictFirewalledRequest(request);
}
}
}
方法會判斷請求的方法是否是可允許的
org.springframework.security.web.firewall.StrictHttpFirewall#rejectForbiddenHttpMethod
private void rejectForbiddenHttpMethod(HttpServletRequest request) {
if (this.allowedHttpMethods != ALLOW_ANY_HTTP_METHOD) {
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 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;
}
org.springframework.security.web.firewall.StrictHttpFirewall#rejectedBlocklistedUrls
private void rejectedBlocklistedUrls(HttpServletRequest request) {
Iterator var2 = this.encodedUrlBlocklist.iterator();
String forbidden;
do {
if (!var2.hasNext()) {
var2 = this.decodedUrlBlocklist.iterator();
do {
if (!var2.hasNext()) {
return;
}
forbidden = (String)var2.next();
} while(!decodedUrlContains(request, forbidden));
throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
}
forbidden = (String)var2.next();
} while(!encodedUrlContains(request, forbidden));
throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
}
encodedUrlBlocklist = {[email protected]} size = 18
0 = "//"
1 = ""
2 = "%2F%2f"
3 = "%2F%2F"
4 = "%00"
5 = "%25"
6 = "%2f%2f"
7 = "%2f%2F"
8 = "%5c"
9 = "%5C"
10 = "%3b"
11 = "%3B"
12 = "%2e"
13 = "%2E"
14 = "%2f"
15 = "%2F"
16 = ";"
17 = "\"
decodedUrlBlocklist = {[email protected]} size = 16
0 = "//"
1 = ""
2 = "%2F%2f"
3 = "%2F%2F"
4 = "%00"
5 = "%"
6 = "%2f%2f"
7 = "%2f%2F"
8 = "%5c"
9 = "%5C"
10 = "%3b"
11 = "%3B"
12 = "%2f"
13 = "%2F"
14 = ";"
15 = "\"
private static boolean encodedUrlContains(HttpServletRequest request, String value) {
return valueContains(request.getContextPath(), value) ? true : valueContains(request.getRequestURI(), value);
}
private static boolean decodedUrlContains(HttpServletRequest request, String value) {
if (valueContains(request.getServletPath(), value)) {
return true;
} else {
return valueContains(request.getPathInfo(), value);
}
}
private static boolean valueContains(String value, String contains) {
return value != null && value.contains(contains);
}
優先從request.getContextPath()
裏面取值,如果存在黑名單,即返回flase拋異常。
org.springframework.security.web.firewall.StrictHttpFirewall#rejectedUntrustedHosts
private void rejectedUntrustedHosts(HttpServletRequest request) {
String serverName = request.getServerName();
if (serverName != null && !this.allowedHostnames.test(serverName)) {
throw new RequestRejectedException("The request was rejected because the domain " + serverName + " is untrusted.");
}
}
org.springframework.security.web.firewall.StrictHttpFirewall#isNormalized(java.lang.String)
private static boolean isNormalized(String path) {
if (path == null) {
return true;
} else {
int slashIndex;
for(int i = path.length(); i > 0; i = slashIndex) {
slashIndex = path.lastIndexOf(47, i - 1);
int gap = i - slashIndex;
if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
return false;
}
if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
return false;
}
}
return true;
}
}
檢查request.getRequestURI() request.getContextPath() request.getServletPath() request.getPathInfo() 不允許出現.
, /./
或者 /.
對 request.getRequestURI();
調用org.springframework.security.web.firewall.StrictHttpFirewall#containsOnlyPrintableAsciiCharacters
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
int length = uri.length();
for(int i = 0; i < length; ++i) {
char ch = uri.charAt(i);
if (ch < ' ' || ch > '~') {
return false;
}
}
return true;
}
不允許出現的特殊字符
!
"
#
$
%
&
'
(
)
*
+
,
-
.
/
:
;
<
=
>
?
@
[
\
]
^
_
`
{
|
}
~
獲取filters,調用virtualFilterChain.doFilter
走入下面會遍歷調用doFilter,走入 Filter執行鏈
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.currentPosition == this.size) {
if (FilterChainProxy.logger.isDebugEnabled()) {
FilterChainProxy.logger.debug(LogMessage.of(() -> {
return "Secured " + FilterChainProxy.requestLine(this.firewalledRequest);
}));
}
this.firewalledRequest.reset();
this.originalChain.doFilter(request, response);
} else {
++this.currentPosition;
Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
if (FilterChainProxy.logger.isTraceEnabled()) {
FilterChainProxy.logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(), this.currentPosition, this.size));
}
nextFilter.doFilter(request, response, this);
}
}
org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.invoke(new FilterInvocation(request, response, chain));
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} else {
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
調用 super.beforeInvocation(filterInvocation);
org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource#getAttributes
org.springframework.security.web.util.matcher.RegexRequestMatcher#matches
進行正則匹配。
這裏先換成漏洞的配置
@Configuration
@EnableWebSecurity
public class AuthConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.regexMatchers("/admin/.*").authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults());
return http.build();
}
使用regexMatchers即會用org.springframework.security.web.util.matcher.RegexRequestMatcher#matches
類來處理編寫的規則
訪問/admin/123
是命中這條規則的,在配置裏面這個規則是需要走認證的。
但是訪問``/admin/123%0d`這裏的正則匹配是爲flase,並沒有命中這條規則,從而走到下一條規則從而實現繞過。
這裏的問題就在於用了.*
的正則去匹配,而傳入數據%0d的話是匹配不上的。Pattern默認的規則是不匹配\r\n
等的。
public class test {
public static void main(String[] args) {
String regex = "a.*b";
//輸出true,指定Pattern.DOTALL模式,可以匹配換行符。
Pattern pattern1 = Pattern.compile(regex,Pattern.DOTALL);
boolean matches1 = pattern1.matcher("aaabbb").matches();
System.out.println(matches1);
boolean matches2 = pattern1.matcher("aa\nbb").matches();
System.out.println(matches2);
//輸出false,默認點(.)沒有匹配換行符
Pattern pattern2 = Pattern.compile(regex);
boolean matches3 = pattern2.matcher("aaabbb").matches();
boolean matches4 = pattern2.matcher("aa\nbb").matches();
System.out.println(matches3);
System.out.println(matches4);
}
}
//true
//true
//true
//false
但是如果加上Pattern.DOTALL
參數的話即便有\n,也會進行匹配。所以後面版本修復使用到了Pattern.DOTALL
https://github.com/spring-projects/spring-security/commit/70863952aeb9733499027714d38821db05654856
參考
Spring Security的一個簡單auth bypass和一些小筆記
Spring Security原理篇(四) FilterChainProxy
結尾
Spring Securit要比Shiro要安全不少,自帶的StrictHttpFirewall
把一些可測試的危險字符限制比較死。