취미겸생업

[Spring boot] CSRF 토큰과 Session 생성 타이밍 불일치 본문

IT/트러블슈팅

[Spring boot] CSRF 토큰과 Session 생성 타이밍 불일치

nohumb 2026. 4. 14. 00:35
SMALL

문제 상황

로컬 개발 환경에선 문제 없던 Thymeleaf Spring MVC에서 비로그인 시 Post Detail 로딩이 불가한 상황이 발생했다.

  • 클라이언트 에러
Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING
  • 애플리케이션 에러
s.e.ErrorMvcAutoConfiguration$StaticView : Cannot render error page for request [/posts/2] as the response has already been committed. As a result, the response may have the wrong status code.
  • 추측
    • 개발 환경과 배포 환경의 큰 차이는, 배포 환경은 Docker 상에서 NGINX와 같은 Reverse Proxy를 통해 요청이 들어오며 로컬 환경에 비해 CPU, Memory가 심각하게 모자란다는 점.

전제

  • Spring Security의 기본 설정은 POST 요청 시 CSRF 토큰을 검사해 CSRF 공격을 방지한다.
  • 이 과정에서 Thymeleaf HTML 내에 포함된 GET을 제외한 모든 메서드의 Form에 Spring Security가 CSRF 토큰을 hidden으로 자동으로 포함해준다. (th:action 속성 사용 시)
  • 배포 환경의 Reverse Proxy(Nginx) 아키텍처는 데이터 전송의 효율성을 위해 백엔드의 응답을 즉시 스트리밍(Chunked)한다

문제점

  1. Spring Security 6+부터는 CSRF 토큰이 필요할 때만 생성되어 사용되는 지연로딩 방식이다
  2. 배포 환경의 Reverse Proxy(Nginx) 아키텍처는 데이터 전송의 효율성을 위해 백엔드의 응답을 즉시 스트리밍(Chunked)한다
  3. th:action 속성을 사용하는 Form 태그를 렌더링 하는 시점에 지연 로딩된 CSRF 토큰을 생성하기 위해 세션 쿠키 헤더(Set-Cookie)를 추가하려다 프로토콜 위반으로 예외가 발생한다
  4. 이미 HTML 본문 일부가 렌더링이 진행되었기 때문에 에러 페이지로도 Forwarding이 불가능해 ERR_INCOMPLETE_CHUNKED_ENCODING 에러가 브라우저에 발생한다

요약 : Spring Security 6+의 CSRF 토큰 지연로딩과 NGINX Streaming(Chunked) 방식의 시너지

  • 로컬 순수 톰캣에선 8KB 가량의 버퍼 사이즈가 찰 때 까지 Flush가 발생하지 않지만 배포 환경의 경우 네트워크 지연이나 NGINX의 즉시 스트리밍 방식에 의해 버퍼가 Flush 되는 시점이 빠르기 때문에 환경에 따라 동작이 달라진다

해결법

  1. 요청마다 명시적으로 세션 생성하기

     @GetMapping("/posts/{id}")
     public String postDetail(
         ...
         HttpServletRequest request
     ) {
         request.getSession(); // 세션 조회, 세션이 존재하지 않을 경우 생성
    
         ...
     }
    
    • 장점 : 코드 한 줄로 해결되고, 컨트롤러마다 명시적으로 세션 생성 시점을 정의할 수 있다
    • 단점 : 보일러 플레이트
  2. Spring Security의 HttpSession 정책을 Always로 설정

     @Bean
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         http
             ...
             .sessionManagement(session -> session
             .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
             );
    
         return http.build();
     }
    • 장점 : 모든 요청에 대해 사전에 강제적으로 세션을 생성해서 보일러 플레이트를 방지할 수 있다
    • 단점 : 세션이 필요없는 요청도 세션을 생성하기 때문에 메모리 점유가 높아져 오버헤드가 생길 수 있고, 동시 접속자가 많을 경우 네트워크 부하가 증가한다.
  3. Thymeleaf Chunk 단위 렌더링 비활성화

     spring:
       thymeleaf:
         servlet:
           produce-partial-output-while-processing: false
    • 장점 : 부분 렌더링을 원천 차단하는 설정 하나 만으로 문제 해결이 가능하다
    • 단점 : 사용자가 처음 화면을 보기까지 시간이 걸려 UI/UX 문제가 있으며, 다수의 사용자가 동시 접속 시 힙(Heap) 메모리에 부하가 있을 수 있다
  4. Cookie 기반 CSRF 설정

     @Bean
     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
             .csrf(csrf -> csrf
                 .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
             );
         return http.build();
     }
    • 장점 : 쿠키 기반의 CSRF를 사용해서 Session 방식의 메모리 오버헤드에서 자유롭다, Stateless 환경에 잘 어울리기에 비교적 현대적이다
    • 단점 : HttpOnly Cookie가 아니기 때문에 XSS 공격에 의한 CSRF 토큰 탈취에 취약해진다. 이스케이핑 문자에 대한 방어 전략이 필요하다.

결론

포트폴리오를 On-Premise 환경에서 계속 돌려야 하는 입장에서 성능과 안정성 모두 쉽게 포기할 수 없었다.

모든 텍스트 입력에 대한 XSS CSRF 탈취 공격에 대한 방어 전략을 적용하기도 부담스러웠다.

따라서 문제 발생 시 직접 Session 생성 시점을 정의하기로 결정했다..

완벽한 정답을 찾지 못했다.

LIST