antMatchers vs. mvcMatchers

antMatchers vs. mvcMatchers

CVE-2016-5007


CVE-2016-5007


Both Spring Security 3.2.x, 4.0.x, 4.1.0 and the Spring Framework 3.2.x, 4.0.x, 4.1.x, 4.2.x rely on URL pattern mappings for authorization and for mapping requests to controllers respectively. Differences in the strictness of the pattern matching mechanisms, for example with regards to space trimming in path segments, can lead Spring Security to not recognize certain paths as not protected that are in fact mapped to Spring MVC controllers that should be protected. The problem is compounded by the fact that the Spring Framework provides richer features with regards to pattern matching as well as by the fact that pattern matching in each Spring Security and the Spring Framework can easily be customized creating additional differences.


업계에서 흔히 RESTful이라고 부르는 API 설계 방식에서는 슬래시(/)를 통해 리소스 구조를 표현한다.

흔히 볼 수 있는 방식인데, 리눅스의 파일 시스템도 이와 같은 방식으로 구분을 하고 있다.

궁금하다면, 터미널에서 pwd를 입력해보자.

아무튼, URI에서 슬래시는 이렇게 특별한 역할을 하고 있기 때문에, 이를 URI 맨 뒤쪽에 넣는다면 혼동이 생길 수 있다.

위와 같은 이유로 스프링 시큐리티에서 기본적으로 사용하는 antMatchers는 URI 맨 뒤에 슬래시가 붙어있다면, 이를 제대로 검증하지 못한다.

이게 무슨 말이냐면, 코드로 보자.


@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable()
            .csrf().disable()
                
            // 블랙리스트 방식
            .authorizeRequests()
            .antMatchers(GET, "/v1/api/members").authenticated()
            .anyRequest().permitAll();
    }
}


/v1/api/members로 오는 GET 방식의 요청은 인증된 상태여야만 허용되게끔 설정돼있다.

이 상태에서 다음과 같은 요청을 보내보았다.


@SpringBootTest
@AutoConfigureMockMvc
class DemoApplicationTests {
    @Autowired
    MockMvc mockMvc;

    @Test
    void contextLoads() throws Exception {
        mockMvc.perform(get("/v1/api/members/"))
            .andDo(print());
    }
}


보다시피 요청 URI 맨 뒤에 슬래시를 하나 더 붙여버렸다.

실행하면 403이 응답될까? 아닐까?


MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /v1/api/members/
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = me.siro.demo.MemberController
           Method = me.siro.demo.MemberController#members()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = application/json
             Body = [{"name":"Liam"},{"name":"\tNoah"},{"name":"\tOliver"},{"name":"\tElijah"},{"name":"\tWilliam"},{"name":"\tJames"},{"name":"\tBenjamin"},{"name":"\tLucas"},{"name":"\tHenry"},{"name":"\tAlexander"},{"name":"Olivia"},{"name":"Emma"},{"name":"Ava"},{"name":"Charlotte"},{"name":"Sophia"},{"name":"Amelia"},{"name":"Isabella"},{"name":"Mia"},{"name":"Evelyn"},{"name":"Harper"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []


놀랍게도 403이 아닌 200이 응답됐으며, 모든 사용자의 정보가 외부에 노출되어버렸다.

이처럼 모든 엔드포인트를 허용 상태로 두고 몇몇 엔드포인트만 콕 집어서 인증필요 상태로 관리하는 방식을 블랙리스트 방식이라고 부르는데, 이는 스프링 시큐리티에서 권장하는 방식이 아니다.

스프링 시큐리티에서는 모든 엔드포인트를 인증필요 상태로 관리하고, 몇몇 엔드포인트만 콕 집어서 허용 상태로 관리하는 화이트리스트 방식을 권장하고 있다.

즉, 화이트리스트 방식으로 코드를 작성했다면 일단 위와 같은 취약점이 생기지는 않는다.


@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable()
            .csrf().disable()
                
            // 화이트리스트 방식
            .authorizeRequests()
            .antMatchers(POST, "/v1/api/members").permitAll()
            .anyRequest().authenticated();
    }
}


하지만, 업무규칙으로 인해 블랙리스트 방식으로 코드를 작성해야만 하는 경우도 있을것이다.

그리고 그런 상황에 위와 같은 정보를 알지 못한다면, 보다시피 보안 취약점이 생길 여지가 분명히 존재한다.

이러한 상황에 대처하기 위해 두가지 방법이 존재한다.


방법 1. URI에 와일드카드를 붙인다


@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable()
            .csrf().disable()
            .authorizeRequests()
            .antMatchers(GET, "/v1/api/members/**").authenticated()
//                                            ^^^ - 와일드카드 추가
            .anyRequest().permitAll();
    }
}


URI 맨 뒤에 /**를 추가했다.

이렇게 하면 antMatchers로도 위와 같은 보안 취약점이 발생하지 않는다.


MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /v1/api/members/
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {SPRING_SECURITY_SAVED_REQUEST=DefaultSavedRequest [http://localhost/v1/api/members/]}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 403
    Error message = Access Denied
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []


요청 URI 맨 뒤에 슬래시를 추가하여 요청했음에도 403과 함께 요청이 디나이 된 모습을 볼 수 있다.


방법 2. mvcMatchers를 사용한다


@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable()
            .csrf().disable()
            .authorizeRequests()
            .mvcMatchers(GET, "/v1/api/members").authenticated()
//           ^^^^^^^^^^^ - antMatchers 대체
            .anyRequest().permitAll();
    }
}


MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /v1/api/members/
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {SPRING_SECURITY_SAVED_REQUEST=DefaultSavedRequest [http://localhost/v1/api/members/]}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 403
    Error message = Access Denied
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []


역시 마찬가지로 요청 URI 맨 뒤에 슬래시를 추가하여 요청했음에도 403과 함께 요청이 디나이 된 모습을 볼 수 있다.


📕 Reference




© 2022. All rights reserved.