본문 바로가기

카테고리 없음

[Spring Boot] Security6 CSRF (Cross-Site Request Forgery) 및 설정 방법

 

 

 

CSRF(Cross-Site Request Forgery)는 사용자가 의도하지 않은 작업을 웹 애플리케이션에서 수행하도록 유도하는 공격 기법입니다. 이 공격은 사용자의 신원을 직접적으로 훔치지 않지만, 사용자가 자신도 모르는 사이에 행동을 취하도록 합니다. 이를 통해 공격자는 사용자의 권한을 이용해 악의적인 요청을 수행할 수 있습니다.

단계별 CSRF 공격 설명

단계 1: 사용자가 Netflix에 로그인

  • 사용자는 netflix.com에 로그인합니다.
  • Netflix 서버는 사용자의 인증을 확인한 후, 해당 사용자의 브라우저에 쿠키를 생성하고 저장합니다.
  • 이 쿠키는 netflix.com 도메인에 대해 유효하며, 다른 도메인에서는 접근할 수 없습니다.

단계 2: 사용자가 악성 웹사이트(evil.com)에 방문

  • 영화를 본 후, 사용자는 evil.com이라는 웹사이트를 방문합니다.
  • 이 사이트는 해커가 운영하며, 사용자가 흥미를 가질 만한 콘텐츠(예: "아이폰 90% 할인")를 제공합니다.
  • 사용자는 같은 브라우저의 다른 탭에서 evil.com에 접속합니다.

단계 3: 사용자가 악성 링크를 클릭

  • 사용자가 evil.com에서 제공하는 링크를 클릭하면, 그 뒤에 숨겨진 악성 코드가 실행됩니다.
  • 이 링크는 Netflix 서버에 이메일 변경 요청을 보내도록 설계되었습니다.
  • 브라우저는 이 요청을 netflix.com으로 보내며, 함께 저장된 쿠키도 전송합니다.
  • Netflix 서버는 요청이 실제 netflix.com에서 온 것인지, 악성 사이트에서 온 것인지 구분하지 못합니다.
  • 결과적으로 사용자의 이메일이 해커가 지정한 이메일로 변경됩니다.

시나리오 예시

  1. 로그인 및 쿠키 생성:
    • netflix.com에 로그인 시도
    • Netflix 서버가 사용자 브라우저에 쿠키를 저장
  2. 악성 웹사이트 방문:
    • 사용자가 evil.com에 접속
    • 해커는 사용자가 링크를 클릭하도록 유도
  3. 악성 링크 클릭 및 이메일 변경 요청:
    • 사용자가 링크를 클릭하여 악성 코드를 실행
    • 브라우저가 netflix.com에 POST 요청을 보내고, 쿠키를 함께 전송
    • Netflix 서버가 요청을 처리하여 사용자의 이메일을 변경

방어 방법

  1. CSRF 토큰 사용: 각 요청에 고유한 토큰을 포함시켜 요청의 유효성을 검증합니다.
  2. SameSite 쿠키 속성 사용: 쿠키가 동일한 사이트 내에서만 전송되도록 제한합니다.
  3. Referrer 검증: 요청의 출처를 검증하여 신뢰할 수 있는 도메인에서 온 요청만 처리합니다.

CSRF 공격은 사용자가 자신도 모르는 사이에 악의적인 요청을 수행하게 만들기 때문에 매우 위험합니다. 이를 방지하기 위해서는 적절한 보안 조치를 취하는 것이 중요합니다.

 

 

설정 방법

package com.example.loans_domain.config;

import com.example.loans_domain.filter.CsrfCookieFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Collections;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 토큰을 요청 속성에 설정하는 핸들러 생성
        CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
        requestHandler.setCsrfRequestAttributeName("_csrf");

        http
                .securityContext((context) -> context.requireExplicitSave(false))
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
                .cors((cors) -> cors.configurationSource(new CorsConfigurationSource() {
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration config = new CorsConfiguration();
                        config.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
                        config.setAllowedMethods(Collections.singletonList("*"));
                        config.setAllowCredentials(true);
                        config.setAllowedHeaders(Collections.singletonList("*"));
                        config.setMaxAge(3600L);
                        return config;
                    }
                }))
                .csrf((csrf) -> csrf
                        // CSRF 토큰 요청 핸들러 설정
                        .csrfTokenRequestHandler(requestHandler)
                        // /register 경로에 대해 CSRF 보호 비활성화
                        .ignoringRequestMatchers("/register")
                        // CSRF 토큰을 쿠키로 저장, HttpOnly 설정 비활성화
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                )
                // CSRF 쿠키 필터를 BasicAuthenticationFilter 이후에 추가
                .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards").authenticated()
                        .requestMatchers("/notices", "/contact", "/h2-console/**", "/register").permitAll()
                )
                .httpBasic(Customizer.withDefaults())
                .formLogin(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
package com.example.loans_domain.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class CsrfCookieFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 요청 속성에서 CSRF 토큰을 가져옴
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        if (csrfToken != null && csrfToken.getHeaderName() != null) {
            // CSRF 토큰을 응답 헤더에 설정
            response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
        }

        // 다음 필터로 요청과 응답을 전달
        filterChain.doFilter(request, response);
    }
}

 

Spring Security에서 CSRF 보호를 비활성화 (csrf().disable()) 하는 것은 보안상 매우 위험할 수 있습니다. 이는 애플리케이션이 CSRF(Cross-Site Request Forgery) 공격에 취약해질 수 있기 때문입니다.

 

CSRF 보호 비활성화:

  • csrf.ignoringRequestMatchers("/register")는 /register 경로에 대해 CSRF 보호를 적용하지 않도록 설정합니다.
  • 이는 해당 경로에 대한 요청이 CSRF 토큰 없이도 처리될 수 있음을 의미합니다.

왜 특정 경로에서 CSRF를 비활성화하는가?

특정 상황에서 CSRF 보호를 비활성화하는 것이 필요할 수 있습니다. 예를 들어, 다음과 같은 경우에 해당할 수 있습니다:

  • 외부 시스템과의 통합: 외부 시스템이 CSRF 토큰을 포함할 수 없는 경우, 특정 엔드포인트에 대해 CSRF 보호를 비활성화해야 할 수 있습니다.
  • 공개 API: 특정 공개 엔드포인트는 CSRF 보호가 필요하지 않을 수 있습니다.

 

현재 설정에서는 CSRF 토큰을 쿠키헤더 모두에 저장 및 전달하도록 설정되어 있습니다. 이 방식은 클라이언트 측에서 CSRF 토큰을 쉽게 접근하고 사용할 수 있게 합니다.

설명:

  1. **CookieCsrfTokenRepository**를 사용하여 CSRF 토큰을 쿠키에 저장:
    • CookieCsrfTokenRepository는 CSRF 토큰을 쿠키로 저장하여, 클라이언트 측에서 접근할 수 있도록 합니다.
    • withHttpOnlyFalse() 설정을 통해 JavaScript에서도 쿠키에 접근할 수 있도록 합니다.
  2. **CsrfCookieFilter**를 사용하여 CSRF 토큰을 응답 헤더에 설정:
    • CsrfCookieFilter는 요청 속성에서 CSRF 토큰을 가져와 응답 헤더에 설정합니다.
    • 이를 통해 클라이언트는 응답 헤더에서 CSRF 토큰을 읽어올 수 있습니다.

 

securityContext 설정

http.securityContext((context) -> context.requireExplicitSave(false))

설명:

  • securityContext: Spring Security의 SecurityContext는 현재 인증된 사용자에 대한 보안 정보를 저장하는 컨테이너입니다.
  • requireExplicitSave(false):
    • 기본적으로 Spring Security는 보안 컨텍스트를 명시적으로 저장하지 않아도 자동으로 저장합니다.
    • requireExplicitSave(false)는 보안 컨텍스트를 자동으로 저장하도록 설정하는 것입니다.
    • 이는 보안 컨텍스트가 변경되면, 자동으로 현재 HTTP 세션에 보안 컨텍스트가 저장됩니다.

예시:

  • 사용자가 애플리케이션에 로그인하면, Spring Security는 사용자의 인증 정보를 SecurityContext에 저장합니다. 이 설정은 이러한 보안 컨텍스트가 자동으로 세션에 저장되도록 합니다.

 

sessionManagement 설정

http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))

설명:

  • sessionManagement: 이 설정은 애플리케이션의 세션 관리 정책을 정의합니다.
  • SessionCreationPolicy.ALWAYS:
    • Spring Security는 요청이 들어올 때마다 새로운 HTTP 세션을 생성합니다.
    • 이는 세션이 항상 생성되며, 세션이 없으면 새로운 세션을 만듭니다.

SessionCreationPolicy의 다른 옵션:

  1. ALWAYS: 항상 세션을 생성합니다.
  2. IF_REQUIRED: 필요할 때만 세션을 생성합니다. (기본 설정)
  3. NEVER: Spring Security는 세션을 생성하지 않지만, 이미 존재하는 세션을 사용할 수는 있습니다.
  4. STATELESS: Spring Security는 세션을 생성하거나 사용하지 않습니다. 주로 REST API와 같은 상태 비저장 애플리케이션에서 사용됩니다.

예시:

  • 사용자가 애플리케이션을 방문할 때마다 새로운 세션이 생성됩니다. 이는 보안 강화 측면에서 세션 고정 공격(session fixation attack) 등을 방지하는 데 유용할 수 있습니다.

 

클라이언트 측에서 CSRF 토큰 사용

클라이언트 측에서는 이 토큰을 추출하여 다음 요청에 포함시킬 수 있습니다. 예를 들어, Vue.js 또는 React와 같은 프레임워크에서 Axios를 사용하여 요청을 보낼 때 다음과 같이 할 수 있습니다:

 

import axios from 'axios';

// CSRF 토큰을 쿠키에서 추출
const csrfToken = document.cookie.split('; ')
    .find(row => row.startsWith('XSRF-TOKEN'))
    .split('=')[1];

// Axios 기본 헤더에 CSRF 토큰을 설정
axios.defaults.headers.common['X-XSRF-TOKEN'] = csrfToken;

// 예시 요청
axios.post('/some-protected-endpoint', { data: 'example' })
    .then(response => {
        console.log('Response:', response);
    })
    .catch(error => {
        console.error('Error:', error);
    });