스프링 시큐리티 CSRF 보호와 CORS 적용

조승빈·2024년 6월 6일

Spring Security

목록 보기
8/11

CSRF 보호 적용

CSRF는 Cross-Site Request Forgery(사이트 간 요청 위조)이다.

CSRF 공격 시나리오는 다음과 같다.

  • 사용자 로그인: 사용자가 신뢰하는 웹사이트에 로그인한다.
  • 악성 사이트 방문: 사용자가 공격자가 제어하는 악성 웹사이트를 방문한다.
  • 악의적인 요청 전송: 악성 사이트는 사용자의 인증 정보를 이용해 신뢰하는 웹사이트로 악의적인 요청을 보낸다.

쉽게 말해 내가 로그인 중인 사이트를 몰래 이용해 원하지 않는 요청을 보낸다.

만약 jwt방식의 로그인을 하는 웹 페이지에서 access token을 쿠키에 넣으면 CSRF 위험이 있기 때문에 대응이 필요하다.

브라우저는 쿠키를 자동으로 보낸다.
즉, 공격자는 access token이 쿠키에 있다면 몰래 요청을 보내도 그 쿠키가 함께 전송되므로 인증이 된 것처럼 보인다.

CSRF보호를 알기 전에 알아야 할 사항은 데이터를 변경하는 작업을 수행하기 위해 사용자가 적어도 한번의 HTTP GET으로 웹페이지를 요청해야 한다는 것이다. 이 때 고유한 토큰을 생성한다. 그리고 헤더에 토큰을 포함하여 통신을 수행한다. 토큰을 통해 다른 시스템이 아닌 애플리케이션에서 요청을 보냈다는 것이다.

CsrfFilter

CSRF 보호의 시작점은 필터 체인의 CsrfFilter이다. CsrfFilter는 요청을 가로채고 있다가 HTTP 방식의 요청을 모두 허용하고 다른 모든 요청에는 토큰이 포함된 헤더가 있는지 확인한다. 토큰이 없거나 잘못된 값일 경우 403을 응답한다.

CsrfFilter는 CsrfTokenRepository 구성 요소를 이용한다.

  • CSRF 토큰 생성: 사용자가 신뢰하는 웹사이트에 로그인할 때, 서버는 고유한 CSRF 토큰을 생성하여 사용자 세션에 저장한다.
  • CSRF 토큰 포함: 모든 민감한 요청(예: 데이터 수정, 삭제, 금융 거래 등)에 대해 CSRF 토큰을 포함하도록 한다. 이 토큰은 폼 데이터나 요청 헤더에 포함된다.
  • CSRF 토큰 검증: 서버는 요청이 들어올 때마다 요청에 포함된 CSRF 토큰이 사용자 세션에 저장된 토큰과 일치하는지 검증한다. 일치하지 않으면 요청을 거부한다.

기본적으로 CsrfTokenRepository는 토큰을 HTTP 세션에 저장하고 랜덤 UUID로 토큰을 생성한다.

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.web.csrf.CookieCsrfTokenRepository

@Configuration
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http
            .csrf { csrf ->
                csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            }
            .authorizeRequests { authorizeRequests ->
                authorizeRequests
                    .antMatchers("/public/**").permitAll()
                    .anyRequest().authenticated()
            }
    }
}

CookieCsrfTokenRepository를 이용하여 CSRF 토큰을 쿠키에 저장하고 이를 클라이언트가 사용할 수 있게 한다.

CORS 이용

CORS는 Cross-Origin Resource Sharing(교차 출처 리소스 공유)이다.
CORS의 필요성은 웹 애플리케이션에서 나온다. 기본적으로 브라우저는 사이트가 로드된 도메인 이외의 도메인에 대한 요청을 제한한다.

예를 들어 example.com 사이트를 열었다면 api.example.com 에 요청하는 것은 허용하지 않는다. 따라서 CORS를 이용해서 일부 조건에서 다른 출처 간의 요청을 허용한다.

CORS 작동방식

애플리케이션이 두 개의 서로 다른 도메인 간에 호출하는 것은 모두 금지된다. 하지만 그러한 호출이 필요한 경우도 있다. CORS를 이용하면 애플리케이션이 요청을 허용할 도메인, 그리고 공유할 수 있는 세부 정보를 지정할 수 있다.

CORS는 HTTP 헤더를 기반으로 작동하며 가장 중요한 헤더는 다음과 같다.

  • Access-Control-Allow-Origin : 요청을 허용할 출처(도메인)을 지정한다. 모든 출처를 허용하려면 ' * '를 이용한다.
  • Access-Control-Allow-Methods : 요청을 허용할 HTTP 메서드를 지정한다.
  • Access-Control-Allow-Headers : 요청을 허용할 HTTP 헤더를 지정한다.

CORS vs CSRF

CORS는 제한을 가하기보다 교차 도메인 호출의 엄격한 제약 조건을 완화하도록 도와주는 기능이다. 그리고 제한이 적용돼도 일부 상황에서 엔드포인트를 호출할 수 있다.

  • 목적
    CORS: 다른 출처에서의 리소스 요청을 허용할지 결정하는 것.
    CSRF 보호: 사용자가 의도하지 않은 요청이 서버로 보내지는 것을 방지하는 것.

  • 적용 대상
    CORS: 주로 클라이언트-서버 간의 출처 간 요청에 적용.
    CSRF 보호: 사용자의 인증된 세션을 보호하는 데 중점.

  • 작동 방식
    CORS: 브라우저와 서버 간의 사전(preflight) 요청과 응답 헤더를 통해 출처 간 요청을 제어.
    CSRF 보호: 서버가 생성한 토큰을 클라이언트가 요청에 포함하고, 이를 서버에서 검증하여 요청의 유효성을 확인.

@CrossOrigin

@CrossOrigin를 이용해서 특정 출처에서 온 요청을 허용할 수 있다.

  1. 클래스 레벨에서 @CrossOrigin 사용
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api")
@CrossOrigin(origins = ["http://localhost:3000"]) // 허용할 출처를 설정합니다.
class ApiController {

    @GetMapping("/data")
    fun getData(): String {
        return "Hello from API"
    }
}

http://localhost:3000에서 온 모든 요청을 허용한다.

  1. 메서드 레벨에서 @CrossOrigin 사용
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api")
class ApiController {

    @GetMapping("/data")
    @CrossOrigin(origins = ["http://localhost:3000"]) // 허용할 출처를 설정합니다.
    fun getData(): String {
        return "Hello from API"
    }
}

/api/data 엔드포인트에만 CORS 설정이 적용된다.

CorsConfigurer

클래스와 메서드에 적용할 수 있지만 CORS 구성을 한곳에서 정의하는 것이 편할 때가 많다.

import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class WebConfig : WebMvcConfigurer {
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**") // 모든 경로에 대해 CORS를 적용
            .allowedOrigins("http://localhost:3000") // 허용할 출처
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드
            .allowedHeaders("Authorization", "Content-Type") // 허용할 요청 헤더
            .exposedHeaders("Custom-Header") // 노출할 응답 헤더
            .allowCredentials(true) // 자격 증명 허용
            .maxAge(3600) // preflight 요청 캐시 시간 ()
    }
}
profile
평범

0개의 댓글