Java安全之Spring Security绕过总结
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 = {HashSet@7373}  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 = {HashSet@7374}  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把一些可测试的危险字符限制比较死。

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号