Spring Security CSRF 프로텍션
Spring Security로 CSRF 프로텍션 적용
최근의 Java기반의 웹 프로젝트는 대부분 Spring(스프링) 프레임워크 기반으로 구현되기 때문에 자연스럽게 Spring Security를 이용하여 보안관련 기능들을 구현하게 됩니다. 그중 CSRF(Cross Site Request Forgery : 사이트 간 요청 위조)는 개발중에는 매우 귀찮은 존재입니다. 웹에서 CSRF 프로텍션은 보안 대책으로 거의 필수적으로 요구되지만 개발 편의성 문제로 개발중에는 전혀 신경쓰지 않거나 비활성화해두는 경우가 많이 있습니다. 결국에는 보안점검을 통해 지적받고 일괄적으로 소스코드를 변경합니다. 하지만 급하게 고친 코드는 항상(?) 문제를 일으킵니다. 또한 운영중에도 잘못된 수정으로 배포후에 예외를 만나는 경우가 많이 있습니다.
CSRF 프로텍션 활성화
먼저 다들 아시는 내용이겠지만 복습차원에서 Spring Security에서 CSRF 기능을 활성화 시키는 방법에 대해서 알아보겠습니다.
dependencies {
//...
compile group: 'org.springframework.security', name: 'spring-security-config', version: '5.1.5.RELEASE'
compile group: 'org.springframework.security', name: 'spring-security-web', version: '5.1.5.RELEASE'
//...
//compile('org.springframework.boot:spring-boot-starter-security') - starter를 이용하는 경우
}
위와같이 Spring Security 의존성을 추가하거나 Spring Boot를 사용하는 경우는 Starter를 통해서 의존성을 추가할 수 있습니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//...Some Configuration
http.csrf();
//...Other Configuration
}
}
설정방법은 위와 같지만 @EnableWebSecurity
에 의해서 Spring Security가 설정되면 기본적으로 CSRF 프로텍션은 활성화되므로 http.csrf();
이 설정은 사실상 필요없습니다만 설정방식을 명시적으로 보여주는 차원에서 위와같이 기술했습니다.
Spring Security의 CSRF 프로텍션은 Http 세션과 동일한 생명주기(Life Cycle)을 가지는 토큰을 발행한 후 Http 요청(PATCH, POST, PUT, DELETE 메소드인 경우)마다 발행된 토큰이 요청에 포함되어(Http헤더 혹은 파라메터 둘중 하나) 있는지 검사하는 가장 일반적으로 알려진 방식의 구현이 설정되어 있습니다.
Spring Security에서는 토큰의 저장소로 Cookie를 사용하는 방식도 아래의 예와같은 설정을 통해서 지원하고 있습니다. 이 경우에는 Http헤더 혹은 파라메터를 통해 전송된 토큰을 쿠키를 통해서 전송된 토큰과 검증하는 방식으로 동작하게 됩니다. API서버 등과 같이 세션을 사용하지 않는 Stateless한 경우에는 어쩔수 없이 채택하는 방식이긴 합니다만 일반적으로 세션의 속성으로 토큰을 보관하는 방식이 좀더 안전하다고 알려져 있으므로 추천드리지 않습니다. 자세한 내용을 설명하는 것은 이 글의 범위를 벗어나므로 간단하게 설명하면 Cookie를 저장소로 사용할 경우 Cookie에 토큰이 주입되거나 토큰의 유출이 발생할 경우 토큰의 무효화를 할 수 없는 등의 취약점이 있습니다. 더 자세한 내용은 Spring Security의 문서를 참조하시기 바랍니다.
예) http.csrf().csrfTokenRepository(new CookieCsrfTokenRepository());
CSRF 토큰의 구조
public interface CsrfToken extends Serializable {
String getHeaderName();
String getParameterName();
String getToken();
}
위와같이 Http헤더의 헤더명과 파라메터명 그리고 토큰으로 구성되어 있습니다. Http헤더를 사용할 경우는 헤더명을 키로 토큰을 값으로 전송하면 되고 파라메터로 전송할 경우는 파라메터명을 키로 역시 토큰을 값으로 전송하면 됩니다.
CSRF 토큰을 Http요청에 포함시키는 방법
CSRF토큰은 Http 세션과 동일한 생명주기를 가지기 때문에 세션속성으로 저장되게 됩니다. Spring Security의 기본구현은 Http 세션에 org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN
와 같이 다소 긴이름으로 저장되기 때문에 세션을 직접 참조하는 방식을 사용하기가 곤란합니다. 따라서 Spring Security는 해당 토큰을 Http요청마다 '_csrf
' 라는 간단한 이름의 Http Request 속성으로 바인딩시켜 사용성을 올려 줍니다.
1. Form의 파라메터
위에서 설명한 바와 같이 Http Request 속성에 바인딩 되어 있기때문에 el표현식을 사용할 수 있는 템플릿 엔진을 사용중이라면 간단하게 Form의 파라메터로 추가할 수 있습니다.
<!-- Some HTML -->
<form action="/some-uri" method="post">
<input type="text" name="some" value="thing" />
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
<input type="submit" />
</form>
<!-- Other HTML-->
위와같이 '_csrf
' 라는 Http Request 속성을 통해서 파라메터 명과 토큰을 취득하여 Form의 숨겨진 요소로 추가하면 간단하게 포함 시킬 수 있습니다.
Spring MVC에서 제공하는
<form:form>
태그를 사용하거나 Thymeleaf 2.1이상이면서@EnableWebSecurity
를 사용한 경우에는CsrfRequestDataValueProcessor
가 적용되어 필요한 form에 자동으로 CSRF 토큰이 포함되게 됩니다.
2. Ajax 요청시 헤더
사용자 경험을 향상시키기 위해서 화면이동을 제외한 처리는 대부분 Ajax를 통한 JSON 요청으로 처리하는 경우가 많습니다. 이경우 위와같이 파라메터로 토큰을 전송할 수 없으므로 Http 헤더를 통해서 전송할 수 있습니다.
$.ajaxPrefilter(function (options) {
var headerName = '${_csrf.headerName}';
var token = '${_csrf.token}';
if (options.method === 'POST') {
options.headers = options.headers || {};
options.headers[headerName] = token;
}
});
위의코드는 jQuery를 사용하는 경우 모든 Ajax Post 요청에 대해 Http 헤더에 CSRF 토큰을 설정하는 코드 입니다. 혹시 초반에는 CSRF설정을 무효화 하고 구현하는 경우에는 Ajax요청을 특정 라이브러리로 단일화 하여 구현하기를 추천드립니다. 그럴 경우에는 위처럼 단순한 설정만으로도 모든 요청에 CSRF 토큰을 추가하는 것이 가능하므로 소스코드 수정을 최소화 할 수 있습니다.
그 이외의 설정
비활성화
CSRF 프로텍션을 비활성화 시키고 싶은경우에는(추천하지 않지만) 아래와 같이 disabled()
를 호출하면 됩니다.
@Override
protected void configure(HttpSecurity http) throws Exception {
//...Some Configuration
http.csrf().disabled();
//...Other Configuration
}
특정 요청만 예외처리
@Override
protected void configure(HttpSecurity http) throws Exception {
//...Some Configuration
http.csrf()
.ignoringAntMatchers()
.ignoringRequestMatchers();
//...Other Configuration
}
Ant Matcher 혹은 Request Matcher를 지정하여 특정 요청은 대상에서 제외할 수 있습니다. 예를들어 외부에서 오는 요청이나 콜백의 경우 제외해야할 필요가 있는 경우가 있습니다.
특정 요청은 적용
@Override
protected void configure(HttpSecurity http) throws Exception {
//...Some Configuration
http.csrf()
.requireCsrfProtectionMatcher();
//...Other Configuration
}
그다지 쓸일이 있어보이지는 않습니다만 특정 요청은 적용되어야 할 경우에는 위의 설정을 사용할 수 있습니다. 예를들어 기본적으로 제외되어있는 GET, HEAD, TRACE, OPTIONS 메소드중 일부 요청을 추가할 수 있을 것 같습니다.
예외처리
토큰검증에 실패 했을경우 403 Http 응답코드가 반환됩니다. 이때 이동할 페이지 혹은 예외처리를 위와같은 설정을 통해 할 수 있습니다. 세션이 만료되어 서버에서 토큰정보를 취득할 수 없는 경우에는 MissingCsrfTokenException
예외가 발생하고 전송된 토큰이 다른 경우에는 InvalidCsrfTokenException
예외가 발생하므로 필요에 따라 적절한 예외처리를 기술하면 되겠습니다.
@Override
protected void configure(HttpSecurity http) throws Exception {
//...Some Configuration
http.exceptionHandling()
.accessDeniedPage("/denied.html")
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {
if (exception instanceof MissingCsrfTokenException) {
//Some Exception Handling
} else if (exception instanceof InvalidCsrfTokenException) {
//Some Exception Handling
}
});
//...Other Configuration
}
이상으로 Spring Security를 통해 CSRF 프로텍션을 적용하는 방법에 대해서 알아보았습니다. 위에서 언급한대로 CSRF 프로텍션은 웹 보안에있어 거의 필수로 요구되므로 가급적 개발초기부터 고려해서 빠지지 않게 적용되도록 합시다.