프론트에 서버에서 발급한 토큰을 redirect_uri
파라미터에 담아, 새로운 경로를 생성하여 리다이렉트하려고 하였다. 요청은 다음의 주소가 된다.
http://localhost:8080/oauth2/authorization/kakao?redirect_uri=http://localhost:8080/api/v1/oauth/redirect
하지만, 서버에서 다음과 같은 에러가 났다. Request가 거부되었다는 건데, 내용을 살펴보니 경로에 포함되면 안되는 문자열이 들어간것 같다.
org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
디버깅을 통해 코드를 뜯어보니, 방화벽인 StrictHttpFirewall
클래스에 FORBIDDEN_DOUBLE_FORWARDSLASH
, 즉 //
가 경로에 포함되면 안될 금지 문자열로 추가되어 있었다.
시큐리티의 기본 구현체인 StrictHttpFirewall
에서는, 악성 URL로 인한 사이드 이펙트를 최소화하기 위해 아래 수많은 문자들이 포함된 URL은 아예 거부하고 있기 때문이다.
잠재적으로 위험한 요청을 거부하거나, 동작을 제어하기 위해 래핑하는데 사용 가능한 인터페이스이다. 해당 인터페이스 구현체들은 FilterChainProxy
에 주입되기 때문에, 모든 요청을 보내기 이전에 호출된다.
만약 요청 URL을 엄격하게 제한하지 않는다면 무슨 일이 일어날까?
curl 'http://localhost:8080/spring-mvc-showcase/resources/%255c%255c%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/%252e%252e%255c/windows/system.ini'
해당 경로로 요청을 한 해커는, 윈도우 시스템 파일의 내용을 응답으로 받게 된다. %252e%252e%255c
는 ..\
의 이중 인코딩 형식이며, %255c%255c
는 \\
의 이중 인코딩 형식이기 때문이다.
서블릿 기본 사양은 contextPath
, servletPath
, pathInfo
, queryString
중에 contextPath
를 제외한 나머지에 대해 검사를 진행한다. 하지만, servletPath
과 pathInfo
간의 구분을 정확하게 정의하지 않기 때문에, 값의 변환에서 서블릿 컨테이너 간에 불일치가 발생한다. 아래 예시를 봐보자.
http://localhost:8080/api/v1/users/1
어떤 서블릿 컨테이너에서는 /1
경로 변수를 구분할 수 있지만, 어떤 컨테이너는 /api/v1/users/1
자체를 servletPath
로 인식하게 되는 것이다.
이렇듯 URI
에서 경로 변수를 구분할 수 없으면 경로 탐색 / 디렉토리 탐색 공격과 같은 잠재적인 공격이 발생할 수 있기 때문에, HttpFirewall
이라는 방어자를 앞단에 두어 서블릿 컨테이너마다 다른 정규화 방식을 일관되게 처리하고 있다고 생각하면 쉽다.
Context Path vs. Servlet Path
스프링 Security – 요청 거부된 예외
스프링 시큐리티 공식 문서를 보니, 역시나 기본값으로 악의적인 것으로 보이는 요청을 거부해 안전성을 높이는 StrictHttpFirewall
이 사용되고 있었다.
이후 스프링 공식 문서를 찾아보니, StrictHttpFirewall
보다 훨씬 더 느슨한 정책을 가지고 있는 DefaultHttpFirewall
이라는 기본 클래스가 존재했다.
DefaultHttpFirewall
는, 단순히 servletPath
와 pathInfo
에 .
, / ./
, /.
와 같은 패턴이 존재하는지 체크하고 그렇다면 예외를 터트리기 때문에 //
는 요청 거부 대상에 포함되지 않을 수 있는 것이다.
따라서 아래와 같이 spring security
에 DefaultHttpFirewall
을 빈으로 추가하고, httpFirewall
필드로 주입해주었더니 문제를 해결할 수 있었다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.httpFirewall(defaultHttpFirewall());
}
@Bean public HttpFirewall defaultHttpFirewall() {
return new DefaultHttpFirewall();
}
...
}