Cross Site Request Forgery (CSRF)
ํ๋ก ํธ์๋์ ๋ฐฑ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ถ๋ฆฌ๋์ด์์ด๋ Cross Site Request Forgery (CSRF)๋ฅผ ๋นํ์ฑํํ์ง ๋ง์.
CSRF ์์ฒด์ ๋ํด์ ์ ๋ชจ๋ฅด๋ ๊ฐ๋ฐ์๋ผ๋ฉด Cross-Site Request Forgery Prevention Cheat Sheet๋ฅผ ์ฐธ๊ณ ํด๋ณด๋๋ก ํ์. ์ ํ๋ฆฌ์ผ์ด์ ๋ณด์ ๊ฐ์ด๋์์ ๋ก๊ทธ์ธ๊ณผ ๋ก๊ทธ์์ ํ์์ ๋ํด์๋ CSRF ํ ํฐ์ ์ฌ์ฉํ ๊ฒ์ฆ์ ์๊ตฌํ๋ ํธ์ด๋ค. ์คํ๋ง ์ํ๋ฆฌํฐ์์๋ CSRF ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ๋ ๋งค์ปค๋์ฆ์ ์ ๊ณตํ์ฌ ์ฝ๊ฒ CSRF ํ ํฐ์ ์ ์ฉํ ์ ์๋ค.
- HttpSessionCsrfTokenRepository
- CookieCsrfTokenRepository
- XorCsrfTokenRequestAttributeHandler
- XorCsrfChannelInterceptor
- CsrfFilter
- CsrfLogoutHandler
์คํ๋ง ์ํ๋ฆฌํฐ์ SecurityFilterChain์ ๊ตฌ์ฑํ๋ ๊ณผ์ ์ CsrfConfigurer
๋ฅผ ์ดํด๋ณด๋ฉด CsrfFilter
๋ฅผ ํํฐ์ ๋ฑ๋กํ๋๋ฐ CsrfTokenRepository์ CsrfTokenRequestHandler๊ฐ ์ฌ์ฉ๋๋๋ก ์ ๋ฌ๋๋ค. ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด๋ณด๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก๋ ์คํ๋ง ์ํ๋ฆฌํฐ 6 ๋ถํฐ HttpSessionCsrfTokenRepository
์ XorCsrfTokenRequestAttributeHandler
๊ฐ ์ฌ์ฉ๋๋๋ก ๋์ด์์ผ๋ฉฐ HTTP๊ฐ ์๋ ์น์์ผ์ ์ํ ๋ณด์ ์ค์ ์(@EnableWebSocketSecurity)์๋ XorCsrfChannelInterceptor
์ด ์ ์ฉ๋์ด ๋์ํ๋ค.
@Configuration
public SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
}
}
CSRF ํ ํฐ API ์๋ํฌ์ธํธ
๋ฐฑ์๋์ ํ๋ก ํธ์๋ ์ ํ๋ฆฌ์ผ์ด์
์ด ๋ถ๋ฆฌ๋์ด์๋ค๊ณ ํด์ HttpOnly ์์ฑ์ด ์ง์ ๋์ง ์์ CSRF ์ฟ ํค๋ฅผ ์ ๋ฌํ๊ธฐ ์ํด CookieCsrfTokenRepository
๋ฅผ ์ฌ์ฉํ ํ์๋ ์๋ค. ํ๋ก ํธ์๋ ์ ํ๋ฆฌ์ผ์ด์
์ ์ํ CSRF ํ ํฐ์ด ํ์ํ๋ค๋ฉด ์๋์ ๊ฐ์ CSRF ํ ํฐ์ ์๋ตํด์ฃผ๋ API๋ฅผ ๋ง๋ค์ด์ ์ ๊ณตํ์. ๊ธฐ๋ณธ์ ์ผ๋ก GET ์์ฒญ์ ์์ ํ ๋ฉ์๋๋ก ๊ฐ์ฃผํ์ฌ ์คํ๋ง ์ํ๋ฆฌํฐ๋ CSRF ํ ํฐ์ ๋ํ ๊ฒ์ฆ์ ์ฒ๋ฆฌํ์ง ์๋๋ค.
@RestController
public class CsrfController {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken csrfToken) {
return csrfToken;
}
}
์ ํ๋ฆฌ์ผ์ด์ ๋ณด์ ๊ฐ์ด๋์์๋ ๋ก๊ทธ์ธ๊ณผ ๋ก๊ทธ์์ ์์ฒญ์ ๋ํด CSRF ๊ณต๊ฒฉ์ ๋ํ ๋ฐฉ์ด๋ฅผ ์๊ตฌํ๋ค. ๊ฐํน ๋ฐฑ์๋์ ํ๋ก ํธ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ถ๋ฆฌ๋์ด JWT์ ๊ฐ์ ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ์ ์ํํ๋ค๋ฉด ๋นํ์ฑํํ๊ฑฐ๋ ์กฐ์นํ ํ์๊ฐ ์๋ค๋ ๊ฒ์ ๊ธฐ๋กํ ๋ธ๋ก๊ทธ๊ฐ ๋ณด์ด๋๋ฐ ์ด๊ฒ์ ์๋ชป๋ ์ ๋ณด์ด๋ค. ๋ธ๋ผ์ฐ์ ์์ ํ ํฐ์ ์ ๋ฌํ ๋ฐฉ๋ฒ์ ์ฟ ํค๋ ๋ณ๋์ ํค๋ ๋ฟ์ด๋ฉฐ ์ฟ ํค๋ ์ฌ์ค ์ ํค๋ ์ค ํ๋์ผ ๋ฟ์ด๋ค.
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
@Override
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
์คํ๋ง ์ํ๋ฆฌํฐ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก GET, HEAD, TRACE, OPTIONS
์ ๋ํด์๋ ์์ ํ ๋ฉ์๋๋ก ํ๋จํ์ฌ CSRF ๊ฒ์ฆ์ ๋ฌด์ํ๋ค. ๋ง์ฝ, GET์ ์์ ํ์ง ์๋ ํ์๋ก ์ฌ์ฉํ๋ค๋ฉด CSRF ๊ฒ์ฆ์ ๋ณ๋๋ก ์ํํด์ผํ๋ค. ์๋ฅผ ๋ค์ด, ์ฝ๊ฒ ๋ก๊ทธ์์ ํ๊ธฐ ์ํด์ POST ์์ฒญ์ด ์๋ GET ์์ฒญ์ผ๋ก ๊ตฌํํ๋ค๋ฉด CSRF ํํฐ์์ ๋ฌด์๋๊ณ ๋์ด๊ฐ๋ฏ๋ก requireCsrfProtectionMatcher
๋ฅผ ์์ ํ์.
CSRF ํ ํฐ์ ์ ๋ฌํ๋ ๋ฐฉ๋ฒ
CSRF ํ ํฐ์ ์ผ๋ฐ์ ์ผ๋ก HTML ํผ ์ ์ก ์ _csrf ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํ๋๋ฐ X-CSRF-TOKEN ๋๋ X-XSRF-TOKEN ํค๋๋ก๋ ์ ๋ฌํ ์ ์๋๋ก ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ง์ํ๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก๋ HttpSessionCsrfTokenRepository ๊ฐ ์ฌ์ฉ๋๋๋ฐ X-CSRF-TOKEN
์ด๋ผ๋ ํค๋๋ฅผ CookieCsrfTokenRepository๋ X-XSRF-TOKEN
ํค๋๋ฅผ ๋งค์นญํ๋ค. ๊ทธ๋ฌ๋๊น, ๊ธฐ๋ณธ์ ์ผ๋ก๋ X-CSRF-TOKEN ํค๋๋ก ์ ๋ฌํด์ผํ์ง๋ง ์ฟ ํค ๊ธฐ๋ฐ์ CookieCsrfTokenRepository๋ฅผ ์ ์ฉํ๋ค๋ฉด X-XSRF-TOKEN ํค๋๋ก ์์ฒญ ์ ์ ๋ฌํด์ผ CSRF ํ ํฐ์ ์ ๋๋ก ๊ฒ์ฆํ ์ ์๋ค.
@FunctionalInterface
public interface CsrfTokenRequestHandler extends CsrfTokenRequestResolver {
void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken);
@Override
default String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(csrfToken, "csrfToken cannot be null");
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
return actualToken;
}
}
์์ CSRF ํ ํฐ์ ์๋ตํ๋ API ์์๋ ํ ํฐ ๋ฟ๋ง ์๋๋ผ ์์ฒญ ํ๋ผ๋ฏธํฐ๋ฅผ ์ ๋ฌํ ์ด๋ฆ๊ณผ ํค๋๋ฅผ ํจ๊ป ์ ๊ณตํด์ค๋ค. ๋ฐ๋ผ์, Axios์ ๊ฐ์ HTTP ์์ฒญ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด XHR ์์ฒญ์ ์ํํ๋ค๋ฉด ์๋์ ๊ฐ์ด ์ ๋ฌํ ์ ์๋ค.
axios.get('/csrf').then(res => {
const csrf = res.data
axios.defaults.headers.post[csrf.headerName] = csrf.token
axios.defaults.headers.put[csrf.headerName] = csrf.token
axios.defaults.headers.delete[csrf.headerName] = csrf.token
})