인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

dohyun_lim님의 프로필 이미지
dohyun_lim

작성한 질문수

스프링 시큐리티

13) 사이트 간 요청 위조 - CSRF, CsrfFilter

csrfTokenRepo 관련 질문입니다.

작성

·

1.6K

·

수정됨

0

안녕하세요 정수원 선생님 질문이 있습니다.

httpSecurity
        .csrf().csrfTokenRepository(new HttpSessionCsrfTokenRepository());

처럼 세션에 저장하는 경우에 응답으로 csrf token 관련 정보들이 response에 존재하지 않는데

해당 경우에는 custom하게 필터를 만들어서 세션에 저장후 응답에 적절한 id(csrfJSessinID?) 를 넣어줘서

나중에 검증할 수 있도록 해야하나요?

 

client 입장에서는 어떻게 csrf token을 http 요청헤더에 넣을 수 있나요?

답변 3

0

dohyun_lim님의 프로필 이미지
dohyun_lim
질문자

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
//                .csrf().csrfTokenRepository(new HttpSessionCsrfTokenRepository());
                .csrf().csrfTokenRepository(new CookieCsrfTokenRepository());
//        .csrf();

        httpSecurity
                .formLogin();

        httpSecurity
                .authorizeRequests()
                .antMatchers("/login","/").permitAll()
                .antMatchers("/user").hasRole("USER")
                .antMatchers("/admin/pay").hasRole("ADMIN")
                .antMatchers("/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
                .anyRequest().authenticated();

        return httpSecurity.build();
    }

imagecookierepo 설정으로 하면 cookie로 잘보내주지만

 

session repo로 설정하게 되면

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .csrf().csrfTokenRepository(new HttpSessionCsrfTokenRepository());
//                .csrf().csrfTokenRepository(new CookieCsrfTokenRepository());
//        .csrf();

        httpSecurity
                .formLogin();

        httpSecurity
                .authorizeRequests()
                .antMatchers("/login","/").permitAll()
                .antMatchers("/user").hasRole("USER")
                .antMatchers("/admin/pay").hasRole("ADMIN")
                .antMatchers("/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
                .anyRequest().authenticated();

        return httpSecurity.build();
    }

image

response header에 아무것도 넘어오지 않습니다.

안녕하세요. dohyun_lim님

앞서 부정확한 정보드려서 죄송합니다.

server에서 csrf 토큰 관리 방식을 HttpSessionCsrfTokenRepository 으로 설정한다면

client app에서 csrf 토큰을 받기위해서는 server에서 구현이 필요할 것으로 보입니다.

 

스프링 시큐리티는 CsrfFilterdoFilterInternal()을 통해서 토큰을 생성하고 repository에 해당 토큰을 저장합니다.

public final class CsrfFilter extends OncePerRequestFilter {

   // ... //

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
         throws ServletException, IOException {
      request.setAttribute(HttpServletResponse.class.getName(), response);
      CsrfToken csrfToken = this.tokenRepository.loadToken(request);
      boolean missingToken = (csrfToken == null);
      if (missingToken) {
         // 토큰 생성 및 저장
         csrfToken = this.tokenRepository.generateToken(request);
         this.tokenRepository.saveToken(csrfToken, request, response);
      }
      request.setAttribute(CsrfToken.class.getName(), csrfToken);
      request.setAttribute(csrfToken.getParameterName(), csrfToken);
      if (!this.requireCsrfProtectionMatcher.matches(request)) {
         if (this.logger.isTraceEnabled()) {
            this.logger.trace("Did not protect against CSRF since request did not match "
                  + this.requireCsrfProtectionMatcher);
         }
         filterChain.doFilter(request, response);
         return;
      }
      String actualToken = request.getHeader(csrfToken.getHeaderName());
      if (actualToken == null) {
         actualToken = request.getParameter(csrfToken.getParameterName());
      }
      if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
         this.logger.debug(
               LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
         AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
               : new MissingCsrfTokenException(actualToken);
         this.accessDeniedHandler.handle(request, response, exception);
         return;
      }
      filterChain.doFilter(request, response);
   }
}

 

repositroy의 구현체인 HttpSessionCsrfTokenRepositorysaveToken()

을 통해 HttpSessioncsrf 토큰을 저장합니다.

추후 HttpSession 에서 csrf 토큰 값을 꺼내어 client app에 제공하면 됩니다.

(개인적인 의견으로 쿠키를 생성하여 제공하는 것 보다는 헤더를 통해서 제공하는 것이 보안측에서 안전할 것으로 판단 됩니다.)

public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {

   @Override
   public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
      if (token == null) {
         HttpSession session = request.getSession(false);
         if (session != null) {
            session.removeAttribute(this.sessionAttributeName);
         }
      }
      else {
         // HttpSession에 토큰 저장
         HttpSession session = request.getSession();
         session.setAttribute(this.sessionAttributeName, token);
      }
   }
}

 

 

테스트 코드는 다음과 같습니다.

MySecurityConfig

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .anyRequest().permitAll();

        http
                .formLogin();

        http
                .csrf().csrfTokenRepository(httpSessionCsrfTokenRepository());
    }

    @Bean
    public HttpSessionCsrfTokenRepository httpSessionCsrfTokenRepository() {
        HttpSessionCsrfTokenRepository csrfRepository = new HttpSessionCsrfTokenRepository();
        // 아래와 같이 설정하지 않으면
        // 기본값은 "org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" 입니다.
        csrfRepository.setSessionAttributeName("CSRF_TOKEN");
        return csrfRepository;
    }
}

 

TestController

@RestController
public class TestController {

    @GetMapping("/csrf")
    public ResponseEntity<String> getOrCreateCsrfToken(HttpServletRequest request) {
        HttpSession session = request.getSession();
        DefaultCsrfToken csrfToken = (DefaultCsrfToken) session.getAttribute("CSRF_TOKEN");

        return ResponseEntity.ok()
                .header(csrfToken.getHeaderName(), csrfToken.getToken()).body("Check your response header!");
    }
}

 

결과

헤더X-CSRF-TOKEN로 토큰(cf2db3f2-fbfe-4fcd-89e4-b2afce2c5185)값이 응답으로 오는 것을 확인 할 수 있습니다.

image

 

감사합니다.

dohyun_lim님의 프로필 이미지
dohyun_lim
질문자

개발하는 쿼카님 신경써주셔서 답변 감사합니다~

좋은 하루되세욤

0

dohyun_lim님의 프로필 이미지
dohyun_lim
질문자

제가 궁금했던것은 spring security 기본 login 페이지를 이용하면 말슴해주신것처럼 csrf token 을 넣는 tag가 존재하고 실제로 chrome debugger를 통해서 봐도 실제 값이 들어가 있는것을 확인 할 수 있었습니다.

 

제가 궁금한것은 client app 과 spring security server가 따로 떨어져있을 때 client app은 csrf token을 어떻게 받아오는가에 대한 궁금함 이었습니다.

chrome debugger나 postman으로 요청을 보냈을 때 response로 JessionId만 쿠키로 받아오지 csrf에 관련된것은 응답으로 받지 못했는데 client 단에서는 어떻게 넣어서 보내는가 가 궁금한것이었습니다.

안녕하세요. dohyun_lim 님

제가 갖고있는 지식이 도움 되고자 답변 드립니다.

spring security는 크게 2가지 방법으로 csrf 토큰을 제공하는 것으로 알고 있습니다.

  1. HttpSessionCsrfTokenRepository 방법

    1. csrf 토큰을 유저의 세션에 저장

  2. CookieCsrfTokenRepository 방법

    1. csrf 토큰을 브라우저 쿠키에 저장

 

client appserverGET 요청을 하게되면 서버에서 csrf토큰헤더에 담아서 client app응답으로 줄것 입니다.

이때 서버 측에서 HttpSessionCsrfTokenRepository을 사용하면 헤더는 X-CSRF-TOKEN 이고,

CookieCsrfTokenRepository를 사용하면 X-XSRF-TOKEN 인것으로 알고 있습니다.

-> 수정: HttpSessionCsrfTokenRepository을 사용하면 서버쪽에서 따로 csrf 토큰을 제공하는 로직을 구현해야 합니다.

 

 

 

HttpSessionCsrfTokenRepository 클래스

public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {

   private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";

   private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";

   private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName()
         .concat(".CSRF_TOKEN");
//...//
}

 

CookieCsrfTokenRepository 클래스

public final class CookieCsrfTokenRepository implements CsrfTokenRepository {

   static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";

   static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";

   static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";

   //...//
}

해당 방법은 쿠키에 csrf를 담아서 보내기 때문에 안전한 방법은 아닌것으로 생각됩니다. 

 

그리고 기본적으로 아무설정하지 않으면 spring securityHttpSessionCsrfTokenRepository 정책을 사용합니다.

public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
      extends AbstractHttpConfigurer<CsrfConfigurer<H>, H> {

   // default HttpSessionCsrfTokenRepository임을 알수 있다.
   private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());

   private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;

   private List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<>();

   private SessionAuthenticationStrategy sessionAuthenticationStrategy;

   private final ApplicationContext context;

   public CsrfConfigurer(ApplicationContext context) {
      this.context = context;
   }

 
   public CsrfConfigurer<H> csrfTokenRepository(CsrfTokenRepository csrfTokenRepository) {
      Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
      this.csrfTokenRepository = csrfTokenRepository;
      return this;
   }
   //...//
}

 

감사합니다.

0

정수원님의 프로필 이미지
정수원
지식공유자

일단 csrf 토큰을 생성해서 저장하는 부분은 시큐리티가 기본적으로 해 주고 있습니다.

제가 정확하게 질문을 이해하지 못해서 그럴 수 있는데 조금 더 상세한 설명 가능할까요?

그리고 클라이언트에서 csrf token 을 담아서 보내는 부분은 서버에서 csrf token form 태크에 넣든지 아니면 스크립트를 사용해서 헤더에 넣든지 구현해 주어야 합니다.

<input type="hidden" name="${ csrf.parameterName }" value="${ csrf.token }">

이부분은 실전프로젝트 편에서 csrf 를 설정하는 챕터가 있으니 참고해 주시기 바랍니다.

dohyun_lim님의 프로필 이미지
dohyun_lim

작성한 질문수

질문하기