Spring/스프링 시큐리티 인 액션

[10장] CSRF 보호와 CORS 적용

꾸준함. 2025. 5. 31. 13:12

주의

이 책은 Spring Security 5 버전을 기준으로 작성되었으므로, Spring Boot 3.X 버전에서는 일부 클래스가 더 이상 사용되지(deprecated) 않을 수 있습니다.

 

1. 애플리케이션에 CSRF (사이트 간 요청 위조) 보호 적용

  • 스프링 시큐리티에 기본적으로 CSRF 보호가 활성화되어 있음
    • CSRF는 광범위한 공격 유형이며 CSRF에 취약한 애플리케이션은 인증 후 사용자가 웹 애플리케이션에서 원치 않는 작업을 실행하게 할 수 있음
    • CSRF에 취약한 애플리케이션을 개발해서 사용자가 원치 않는 작업이 실행되는 상황은 누구도 원하지 않을 것

 

1.1 스프링 시큐리티의 CSRF 보호가 작동하는 방식

  • CSRF 공격은 사용자가 웹 애플리케이션에 로그인했다고 가정하며 사용자는 공격자에게 속아서 작업 중인 같은 애플리케이션에서 작업을 실행하는 스크립트가 포함된 페이지를 염
    • 사용자가 이미 로그인했기 때문에 위조 코드는 이제 사용자를 가장하고 사용자 대신 작업을 수행할 수 있음

 

  • CSRF 보호는 웹 애플리케이션에서 프런트 엔드만 변경 작업 (관례상 GET, HEAD, TRACE, OPTIONS 외의 HTTP 방식)을 수행할 수 있게 보장함
    • 그러면 아래 예제와 같은 외부 페이지가 사용자 대신 작업을 수행할 수 없음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10

 

  • 데이터를 변경하는 작업을 수행하기 위해서는 먼저 사용자가 적어도 한 번은 HTTP GET으로 웹 페이지를 요청해야 함
    • 이때 애플리케이션은 고유한 토큰을 생성
    • 이제부터 애플리케이션은 헤더에 이 고유한 값이 들어 있는 요청에 대해서만 변경 작업 (POST, PUT, DELETE 등)을 수행
    • 애플리케이션이 토큰의 값을 안다는 것은 다른 시스템이 아닌 애플리케이션 자체가 변경 요청을 보낸 증거라고 봄
    • POST, PUT, DELETE를 비롯한 변경 호출을 포함하는 모든 페이지는 응답을 통해 CSRF 토큰을 받고 변경 호출을 할 때 해당 토큰을 이용해야 함

 

  • CSRF 보호의 시작점은 필터 체인의 CsrfFilter라는 한 필터
    • CsrfFilter는 요청을 가로채고 GET, HEAD, TRACE, OPTIONS를 포함하는 HTTP 방식의 요청을 모두 허용하고 다른 모든 요청에는 토큰이 포함된 헤더가 있는지 확인함
    • 해당 헤더가 없거나 헤더에 잘못된 토큰 값이 포함된 경우 애플리케이션은 요청을 거부하고 응답의 상태를 `403 Forbidden`으로 설정

 

  • 요청에 포함된 토큰은 하나의 문자열 값이며, GET, HEAD, TRACE, OPTIONS 외의 HTTP 방식을 이용할 때 요청의 헤더에 해당 토큰을 추가해야 함
    • 토큰을 포함하는 헤더를 추가하지 않으면 애플리케이션은 아래 그림과 같이 요청을 수락하지 않음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/17

 

  • CsrfFilter는 CsrfTokenRepository 구성 요소를 이용해 새 토큰 생성, 토큰 저장 그리고 토큰 검증에 필요한 CSRF 토큰 값을 관리함
    • 기본적으로 CsrfTokenRepository는 토큰을 HTTP 세션에 저장하고 랜덤 UUID로 토큰을 생성함
    • 대부분은 이것으로 충분하지만 구현할 요구 사항이 기본 구현으로 해결되지 않을 경우 CsrfTokenRepository를 직접 구현하는 방법도 있음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/21

 

  • CsrfFilter는 생성된 CSRF 토큰을 HTTP 요청의 _csrf 특성에 추가하며 이것을 알면 CsrfFilter 뒤에서 해당 특성을 찾아 토큰 값을 가져올 수 있음
    • 여기서 찾은 토큰 값을 CSRF 토큰을 지정한 뒤 HTTP POST 방식으로 엔드포인트를 호출하면 정상 응답을 받을 수 있음
    • 또한 CsrfTokenRepository의 기본 구현은 CSRF 토큰의 값을 세션에 저장하므로 세션 ID(JSESSIONID)도 지정해야 함
    • CSRF 토큰을 지정하지 않은 상태로 POST 방식으로 엔드포인트를 호출하면 403 Forbidden 응답을 받음

 

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/22

 

1.2 실제 시나리오에서 CSRF 보호 사용

  • CSRF 보호는 브라우저에서 실행되는 웹 앱에 이용되며, 앱의 표시된 컨텐츠를 로드하는 브라우저가 변경 작업을 수행할 수 있다고 예상될 때 필요
  • 기본 로그인의 경우 스프링 시큐리티는 CSRF 보호를 올바르게 적용해 주고 프레임워크가 CSRF 토큰을 로그인 요청에 추가하는 작업을 처리해 줌
  • 예제 애플리케이션에서는 CSRF 토큰을 올바르게 사용할 때까지 HTTP POST 호출이 작동하지 않는다는 것을 확인하고 웹 페이지의 양식에 CSRF 토큰을 적용하는 방법을 배울 수 있음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/50

 

 

 

1.3 CSRF 보호 맞춤 구성

  • CSRF 보호는 서버에서 생성된 리소스를 이용하는 페이지가 같은 서버에서 생성된 경우에만 이용
  • 기본적으로 CSRF 보호는 GET, HEAD, TRACE, OPTIONS 외의 HTTP 방식으로 호출되는 엔드포인트의 모든 경로에 적용됨
  • 아래 코드는 HTTP POST로 호출하는 엔드포인트 두 개를 추가하지만 둘 중 하나는 CSRF 보호에서 제외하는 예제

 

 

 

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/110

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/90
https://livebook.manning.com/book/spring-security-in-action/chapter-10/83

 

2. CORS (교차 출처 리소스 공유) 이용

  • 기본적으로 브라우저는  사이트가 로드된 도메인 이외의 도메인에 대한 요청을 허용하지 않음
    • i.g. example.com에서 사이트를 열었다면 브라우저는 해당 사이트에서 api.example.com에 요청하는 것을 허용하지 않음

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/149

 

  • 간단히 말하자면 브라우저는 CORS 메커니즘으로 이 엄격한 정책을 완화하고 일부 조건에서 서로 다른 출처 간의 요청을 허용한다고 할 수 있음
    • 특히 프런트엔드와 백엔드가 별도의 애플리케이션인 요즘에는 이를 애플리케이션에 적용해야 할 가능성이 크기 때문에 이를 알아야 함
    • ex) 리액트 프레임워크로 개발하고 example.com 도메인에서 호스팅 하는 프런트엔드 애플리케이션이 api.example.com 등의 다른 도메인에서 호스팅하는 백엔드의 엔드포인트를 호출하는 것이 일반적

 

2.1 CORS 작동 방식

  • 애플리케이션이 두 개의 서로 다른 도메인 간에 호출하는 것은 모두 금지되지만 그렇나 호출이 필요할 때가 있음
    • 이때 CORS를 이용하면 애플리케이션이 요청을 허용할 도메인, 그리고 공유할 수 있는 세부 정보를 지정할 수 있음

 

  • CORS 메커니즘은 HTTP 헤더를 기반으로 작동하며 가장 중요한 헤더는 다음과 같음
    • Access-Control-Allow-Origin: 도메인의 리소스에 접근할 수 있는 외부 도메인을 지정
    • Access-Control-Allow-Methods: 다른 도메인에 대해 접근을 허용하지만 특정 HTTP 방식만 허용하고 싶을 때 일부 HTTP 방식을 지정할 수 있음
    • Access-Control-Allow-Headers: 특정 요청에 이용할 수 있는 헤더에 제한을 추가

 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/155

 

 

  • 애플리케이션은 요청하고 응답을 받을 때 여기에 서버가 수락하는 출처가 나열된 Access-Control-Allow-Origin 헤더가 있다고 예상하는데 스프링 시큐리티의 기본 동작과 같이 해당 헤더가 없으면 브라우저는 응답을 수락하지 않음
  • CORS는 제한을 가하기보다 교차 도메인 호출의 엄격한 제약 조건을 완화하도록 도와주는 기능
  • 그리고 제한이 적용돼도 일부 상황에서는 엔드포인트를 호출할 수 있음
    • 종종 브라우저는 요청을 허용해야 하는지 테스트하기 위해 먼저 HTTP OPTIONS 방식으로 호출하는 경우가 있음
    • 이 테스트 요청을 preflight 요청이라고 하며 해당 요청이 실패하면 브라우저는 원래 요청을 수락하지 않음
    • 리액트와 같은 프레임워크로 클라이언트 쪽 앱을 개발했을 때도 이런 상황이 발생할 수 있음

 

2.2 @CrossOrigin 어노테이션으로 CORS 정책 적용

  • 엔드포인트를 정의하는 메서드 바로 위에 @CrossOrigin 어노테이션을 배치하고 허용된 출처와 메서드를 이용해 구성할 수 있음
  • @CrossOrigin 어노테이션의 장점은 각 엔드포인트에 맞게 손쉽게 CORS를 구성할 수 있다는 것


 

https://livebook.manning.com/book/spring-security-in-action/chapter-10/176

 

  • @CrossOrigin의 값 매개변수는 여러 출처를 정의한느 배열을 받음
    • i.g. @CrossOrigin({"example.com", "example.org"})와 같이 지정할 수 있음
    • 또한 어노테이션의 allowedHeaders 특성과 method 특성으로 허용되는 헤더와 메서드를 설정할 수 있음
    • 출처와 헤더에 asterisk (*)를 이용하면 모든 헤더나 출처를 지정할 수 있지만 해당 방식을 이용할 때는 주의하는 것이 좋음
    • 허용하려는 출처와 헤더를 필터링하고 어떤 도메인이든 애플리케이션의 리소스에 접근하는 코드를 구현하는 것을 허용하지 않는 것을 권장

 

  • 모든 출처를 허용하면 애플리케이션이 XSS 요청에 노출되고 결과적으로 DDOS 공격에 취약해질 수 있음
    • 테스트와 운영에 같은 데이터 센터를 이용하는 잘못 정의된 인프라에서 애플리케이션이 실행되는 경우가 있기 때문에 테스트 환경에서도 모든 출처를 허용하지 않는 것이 바람직함  

 

@CrossOrigin 단점

  • @CrossOrigin으로 엔드포인트가 정의되는 위치에서 직접 규칙을 지정하면 규칙이 투명해지는 장점이 있지만, 코드가 장황해지고 많은 코드를 반복해야 할 수 있다는 단점도 있음
  • 또한 개발자가 새로 구현한 엔드포인트에 어노테이션을 추가하는 것을 잊어버릴 위험도 존재

 

2.3 CorsConfigurer로 CORS 적용

  • 앞서 알아본 것처럼 @CrossOrigin 어노테이션은 쉽게 이용할 수 있지만 CORS 구성을 한 곳에서 정의하는 것이 더 편할 때가 있음
  • 아래 코드를 통해 허용할 출처를 정의하기 위해 구성 클래스를 어떻게 변경해야 하는지 알 수 있음
    • 아래 예제에서는 설명을 간단하게 하기 위해 configure() 메서드에서 람다 식으로 곧바로 CorsConfigurationSource의 구현을 제공했지만 실제 애플리케이션에서는 해당 코드를 다른 클래스로 나누는 것을 권장
    • 실제 애플리케이션의 코드는 훨씬 복잡할 수 있으므로 구성 클래스로 나누지 않으면 코드가 읽기 어려워질 수 있음


 

참고

스프링 시큐리티 인 액션

반응형