이 장에서 다룰 내용
- CSRF 공격 이해하기
CSRF(Cross-Site Request Forgery) 공격은 사용자가 인증된 상태에서 의도하지 않은 요청을 다른 웹사이트로 보내는 공격입니다. 이 공격은 사용자의 브라우저에서 자동으로 실행되는 요청을 악용하여, 공격자가 사용자의 권한을 가로채거나 악용할 수 있게 합니다. - 사이트 간 요청 위조 보호 구현하기
CSRF 공격을 방어하기 위해 Spring Security는 CSRF 보호 메커니즘을 기본적으로 활성화합니다. 이 보호 메커니즘은 요청에 CSRF 토큰을 포함시켜, 서버가 요청이 진짜 사용자의 의도에 의해 발생했는지 확인할 수 있도록 합니다. - CSRF 보호 커스터마이징
Spring Security에서는 기본적으로 CSRF 보호를 제공하지만, 다양한 시나리오에 맞게 이 보호를 커스터마이징할 수 있습니다. 예를 들어, 특정 요청에서 CSRF 보호를 비활성화하거나, 다른 방식으로 토큰을 생성하고 검증하는 등의 방법을 적용할 수 있습니다.
CSRF 보호 개념
Spring Security는 기본적으로 CSRF 보호를 활성화하여, POST 요청을 처리할 때 CSRF 토큰을 요구합니다. 이를 통해 공격자가 사용자를 속여 원하지 않는 POST 요청을 보내는 것을 방지합니다. CSRF 공격은 사용자가 인증된 상태에서 악의적인 사이트가 사용자의 권한을 가로채도록 만듭니다. 예를 들어, 사용자가 로그인 상태일 때, 악의적인 사이트가 사용자의 권한을 사용하여 다른 사이트에 데이터를 삽입하는 등의 방식입니다.
CSRF 토큰 메커니즘
Spring Security는 CSRF 보호를 위해 CSRF 토큰을 사용합니다. 사용자가 인증된 후, 서버는 CSRF 토큰을 클라이언트에게 전달하고, 클라이언트는 이 토큰을 요청에 포함시켜 서버로 보내야 합니다. 서버는 이 토큰을 검증하여 요청이 진짜 사용자의 의도에 의해 발생했는지 확인합니다. 이를 통해 CSRF 공격을 방지할 수 있습니다.
REST 엔드포인트에서 CSRF 보호
RESTful 애플리케이션에서 CSRF 보호를 적용하려면, 클라이언트가 POST, PUT, DELETE와 같은 요청을 보낼 때 CSRF 토큰을 헤더나 요청 본문에 포함시켜야 합니다. 이를 통해 CSRF 공격을 방지할 수 있으며, 클라이언트는 이 토큰을 서버에서 얻은 후, 요청을 보낼 때마다 이를 포함시켜야 합니다.
CSRF 보호의 커스터마이징
CSRF 보호는 Spring Security에서 기본적으로 제공되지만, 필요에 따라 이를 커스터마이징할 수 있습니다. 예를 들어, 특정 엔드포인트에서만 CSRF 보호를 비활성화하거나, 다른 방식으로 토큰을 전달할 수 있는 설정을 할 수 있습니다. 이 장에서는 이러한 설정을 어떻게 적용할 수 있는지에 대해 배울 것입니다.
9.1 How CSRF Protection Works in Spring Security
이 섹션에서는 Spring Security에서 CSRF 보호가 어떻게 작동하는지 설명합니다. CSRF 보호의 기본 메커니즘을 이해하는 것이 중요합니다. 개발자들이 CSRF 보호의 작동 방식을 잘못 이해하고 활성화해야 할 상황에서 비활성화하거나 그 반대로 잘못 사용할 수 있기 때문입니다. 따라서 애플리케이션에 가치를 더하려면 CSRF 보호를 올바르게 사용하는 것이 중요합니다.
다음 시나리오를 고려해봅시다:
직장에서 파일을 저장하고 관리하는 웹 도구를 사용 중입니다. 이 도구를 통해 웹 인터페이스에서 새 파일을 추가하거나, 레코드에 새로운 버전을 추가하거나, 파일을 삭제할 수 있습니다. 어느 날 이메일을 통해 상점의 프로모션을 보게 되며, 링크를 클릭하여 웹 페이지를 엽니다. 그러나 페이지는 비어 있거나 상점의 온라인 샵으로 리다이렉트됩니다. 작업을 마치고 돌아가면, 모든 파일이 사라져 있다는 것을 발견합니다.
무슨 일이 일어난 걸까요? 당신은 이미 직장 애플리케이션에 로그인한 상태였고, 이 애플리케이션에서 파일을 추가, 변경 또는 삭제할 때 사용하는 웹 페이지는 서버의 엔드포인트를 호출합니다. 이메일에서 클릭한 링크는 외부 페이지를 열었고, 그 페이지는 당신의 애플리케이션 백엔드를 호출하여 당신을 대신해 작업(파일 삭제)을 수행한 것입니다.
이 경우, 사용자가 이미 로그인했으므로, 서버는 해당 행동이 당신의 요청으로 간주합니다. 이와 같은 CSRF 공격은 사용자가 웹 애플리케이션에 로그인된 상태에서 발생할 수 있습니다. 공격자는 사용자가 신뢰하는 시스템에서 액션을 실행하는 것처럼 속여 악의적인 요청을 보낼 수 있습니다.
이 공격을 방지하기 위해서는 요청에 고유한 CSRF 토큰을 요구하는 방식으로 보호합니다. CSRF 토큰은 서버에서 생성되어 클라이언트로 전송됩니다. 사용자가 작업을 수행할 때 이 토큰을 서버로 다시 전송하여, 서버는 요청이 의도된 사용자로부터 온 것인지를 확인할 수 있습니다.
CSRF 보호의 작동 방식
Spring Security에서는 CsrfFilter라는 필터를 사용하여 CSRF 보호를 처리합니다. 이 필터는 모든 요청을 가로채고, GET, HEAD, TRACE, OPTIONS와 같은 안전한 HTTP 메소드를 사용하는 요청은 그대로 허용합니다. 반면, POST, PUT, DELETE와 같은 변형 요청을 보낼 때는 CSRF 토큰을 포함한 헤더를 확인하여, 요청이 신뢰할 수 있는 사용자로부터 온 것인지 검증합니다.
이 토큰은 고유한 문자열 값이며, 클라이언트는 이를 서버에서 받은 후 요청을 보낼 때 토큰을 함께 전송해야 합니다. 만약 토큰이 없거나 잘못된 값이 포함되면 서버는 403 Forbidden 응답을 보냅니다.
CSRF 토큰의 관리
CSRF 토큰은 CsrfTokenRepository라는 컴포넌트를 통해 관리됩니다. 기본적으로는 HTTP 세션에 CSRF 토큰을 저장하고, CsrfTokenRepository는 세션에서 이를 가져오거나 새로운 토큰을 생성할 수 있습니다. 사용자가 요청을 보낼 때 이 토큰이 필요한데, 서버는 POST, PUT, DELETE 등의 요청에 대해 올바른 토큰이 포함된 요청만 허용합니다.
CSRF 보호의 흐름
- GET 요청: 페이지 로딩 시 서버는 고유한 CSRF 토큰을 생성하여 클라이언트로 전송합니다.
- 변형 요청: 클라이언트는 POST, PUT, DELETE 요청을 보낼 때 CSRF 토큰을 요청 헤더에 포함시켜 서버로 전송합니다.
- 서버 검증: 서버는 요청에 포함된 CSRF 토큰을 검증하여 요청이 유효한지 확인합니다.
이 방식으로 CSRF 공격을 효과적으로 방어할 수 있습니다.
코드 예제
다음은 Spring Security에서 CSRF 보호를 구현하는 예제입니다. 이 예제는 두 가지 엔드포인트(GET, POST)를 제공하고, CSRF 토큰을 관리하는 방법을 보여줍니다.
@RestController
public class HelloController {
@GetMapping("/hello")
public String getHello() {
return "Get Hello!";
}
@PostMapping("/hello")
public String postHello() {
return "Post Hello!";
}
}
위 예제에서 GET 요청을 통해 페이지를 로드하면 CSRF 토큰이 생성되고, 이후 POST 요청 시 CSRF 토큰을 포함하여 요청을 보내야 합니다.
public class CsrfTokenLogger implements Filter {
private Logger logger = Logger.getLogger(CsrfTokenLogger.class.getName());
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
logger.info("CSRF token " + token.getToken());
filterChain.doFilter(request, response);
}
}
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.addFilterAfter(new CsrfTokenLogger(), CsrfFilter.class)
.authorizeHttpRequests(
c -> c.anyRequest().permitAll()
);
return http.build();
}
}
위 코드에서 CsrfTokenLogger는 CSRF 토큰을 콘솔에 출력하고, ProjectConfig에서 이를 필터 체인에 추가합니다. CSRF 토큰을 포함하지 않은 요청은 403 Forbidden 상태로 거부됩니다.
Postman을 사용하여 CSRF 토큰을 확인하고 사용하는 과정은 다음과 같습니다:
1. CSRF 토큰 얻기
- GET 요청을 통해 페이지 호출: 먼저 CSRF 토큰이 포함된 페이지를 GET 요청으로 호출합니다. 예를 들어, 로그인 페이지나 폼 페이지 등이 될 수 있습니다.
- 응답에서 CSRF 토큰 찾기:
- HTML 응답에서: 응답 본문에서 CSRF 토큰이 <input type="hidden" name="_csrf" value="토큰값">와 같은 형식으로 제공되는 경우가 많습니다.
- 쿠키에서: CSRF 토큰이 쿠키에 포함될 수 있습니다. Postman에서 Cookies 탭을 사용하여 쿠키를 확인할 수 있습니다.
- 헤더에서: 일부 애플리케이션은 CSRF 토큰을 응답 헤더로 제공하기도 합니다.
2. CSRF 토큰을 포함하여 POST 요청 보내기
- POST 요청 생성: 이제 새로운 POST 요청을 생성합니다. 이 요청은 토큰을 포함해야 합니다.
- 요청 헤더에 CSRF 토큰 추가:
- 일반적으로 CSRF 토큰은 요청 헤더에 X-CSRF-TOKEN이라는 이름으로 추가됩니다. 예를 들어, X-CSRF-TOKEN: [토큰값] 형태로 추가합니다.
- 또는, CSRF 토큰이 폼 데이터나 JSON 본문에 포함될 수 있습니다.
- 세션 쿠키 추가: 세션 ID(JSESSIONID 등)가 포함된 쿠키를 요청과 함께 보내야 할 수 있습니다. Postman의 Cookies 탭에서 세션 쿠키를 설정할 수 있습니다.
- 예를 들어, Cookie: JSESSIONID=[세션ID] 형태로 설정할 수 있습니다.
3. 요청 보내기 및 결과 확인
- 요청 전송: 요청을 보내고 서버로부터 응답을 확인합니다.
- 응답 검토: CSRF 토큰이 올바르게 전달되지 않으면, 서버는 일반적으로 403 Forbidden 응답을 반환합니다. 요청이 성공적으로 처리되면, 응답 상태가 200 OK 또는 성공적인 상태 코드가 됩니다.
Postman을 사용하면 CSRF 보호가 활성화된 애플리케이션의 API를 손쉽게 테스트할 수 있습니다. 이를 통해 개발자는 CSRF 보호가 정상적으로 동작하는지 확인하고, 요청이 올바르게 처리되는지 점검할 수 있습니다.
9.2 실제 상황에서 CSRF 보호 적용 방법
CSRF 보호는 웹 애플리케이션에서 보안의 중요한 요소로, 특히 브라우저에서 실행되는 애플리케이션에 필수적입니다. 이 섹션에서는 실제 애플리케이션에 CSRF 보호를 적용하는 방법과 Spring Security에서 CSRF 보호가 어떻게 작동하는지에 대해 설명합니다.
1. CSRF 보호가 필요한 애플리케이션
- 브라우저에서 실행되는 웹 애플리케이션에서 CSRF 보호가 필요합니다. 브라우저는 사용자가 의도하지 않은 변경 작업을 서버에 보내는 것을 막기 위해 CSRF 토큰을 사용합니다.
- 예시: Spring MVC 흐름을 따르는 간단한 웹 애플리케이션. 로그인 작업이나 폼 제출 등의 기능을 제공하는 웹 애플리케이션에서는 CSRF 보호가 활성화되어야 합니다.
2. Spring Security와 CSRF 보호
- 폼 로그인을 설정할 때 Spring Security는 기본적으로 CSRF 보호를 처리합니다. 예를 들어, 로그인 폼은 HTTP POST 요청을 통해 CSRF 토큰을 서버에 제출합니다.
- 디폴트 CSRF 토큰 처리: Spring Security는 로그인 요청에 CSRF 토큰을 자동으로 추가하여, 로그인 폼에서 CSRF 보호가 제대로 적용됩니다.
3. POST 요청을 통한 CSRF 토큰 사용
- 새로운 엔드포인트 추가: 예를 들어, HTTP POST 메서드를 사용하는 /product/add 엔드포인트를 정의합니다. 이 엔드포인트는 사용자가 상품을 추가하는 기능을 담당하며, CSRF 보호를 위해 CSRF 토큰을 요구합니다.
- 폼에서 CSRF 토큰 전송: CSRF 토큰을 전송하려면 HTML 폼에서 <input type="hidden" name="_csrf" value="${_csrf.token}" />와 같은 방식으로 CSRF 토큰을 숨겨진 필드로 전달해야 합니다.
4. 실제 구현 예시
- 프로젝트 설정: Maven 의존성을 추가하여 spring-boot-starter-security, spring-boot-starter-thymeleaf, spring-boot-starter-web을 사용합니다.
- 보안 설정: SecurityFilterChain을 사용하여 로그인 페이지와 인증 필터를 설정합니다. formLogin() 메서드와 addFilterAfter()를 통해 CSRF 보호를 활성화하고, 인증된 사용자만 접근할 수 있도록 설정합니다.
5. 폼과 CSRF 토큰
- 폼에 CSRF 토큰 추가: CSRF 보호를 적용하려면, 폼에 숨겨진 입력 필드를 추가하여 CSRF 토큰을 전송합니다. th:name="${_csrf.parameterName}"와 th:value="${_csrf.token}"을 사용하여 Thymeleaf 템플릿에서 CSRF 토큰을 출력할 수 있습니다.
- HTTP 403 오류 해결: 폼을 제출할 때 CSRF 토큰을 포함하지 않으면 서버는 403 Forbidden 오류를 반환합니다. 이를 해결하려면 폼에 CSRF 토큰을 추가해야 합니다.
6. Postman을 사용한 테스트
- 폼 데이터와 CSRF 토큰: Postman에서 요청 본문에 form-data 형식으로 credential과 함께 _csrf 토큰을 포함하여 전송합니다.
- 세션 관리: 폼 로그인이 성공한 후, 서버에서 응답받은 세션 ID를 요청 헤더에 포함시켜야 합니다. 이를 위해 Postman에서 Cookies 탭을 사용하여 세션 정보를 관리합니다.
7. 보안 고려사항
- HTTP GET을 사용하지 않기: 데이터를 변경하는 작업은 반드시 HTTP GET 메서드를 사용하지 않아야 하며, 이를 HTTP POST, PUT, DELETE 등의 안전한 메서드로 처리해야 합니다.
- 클라이언트 독립적인 아키텍처: CSRF 보호는 프론트엔드와 백엔드가 동일 서버에서 처리될 때 잘 작동하지만, 클라이언트와 백엔드가 독립적인 경우에는 CSRF 토큰을 보내는 방식에 대한 추가 고려가 필요합니다. 예를 들어, Angular, ReactJS, Vue.js와 같은 프레임워크를 사용하는 경우, CSRF 토큰을 어떻게 관리할지에 대해 추가적인 보안 설정이 필요합니다.
이러한 방식으로 CSRF 보호를 설정하고 테스트함으로써, 애플리케이션을 안전하게 보호할 수 있습니다.
9.3 CSRF 보호 사용자 정의
이 섹션에서는 Spring Security가 제공하는 CSRF 보호 솔루션을 사용자 정의하는 방법을 다룹니다. 애플리케이션에는 다양한 요구 사항이 있기 때문에, 프레임워크에서 제공하는 모든 구현은 다양한 시나리오에 쉽게 적응할 수 있을 정도로 유연해야 합니다. Spring Security의 CSRF 보호 메커니즘도 이에 예외는 아닙니다. 이 섹션에서는 가장 자주 발생하는 요구 사항을 적용하여 CSRF 보호 메커니즘을 사용자 정의하는 예제를 제공합니다. 이러한 요구 사항은 다음과 같습니다.
- CSRF가 적용되는 경로 구성
- CSRF 토큰 관리
우리는 서버에서 생성된 리소스를 사용하는 페이지 자체가 동일한 서버에서 생성될 때만 CSRF 보호를 사용합니다. 다른 출처에서 노출된 소비 엔드포인트를 사용하는 웹 애플리케이션이나 모바일 애플리케이션에서는 이 보호가 필요하지 않을 수 있습니다. 모바일 애플리케이션의 경우 OAuth 2 플로우를 사용할 수 있으며, 이에 대해 13장부터 16장까지 논의할 예정입니다.
기본적으로 CSRF 보호는 GET, HEAD, TRACE, OPTIONS 외의 HTTP 메서드로 호출되는 모든 경로에 적용됩니다. 5장에서 CSRF 보호를 완전히 비활성화하는 방법을 배웠습니다. 그러나 일부 애플리케이션 경로에서만 CSRF 보호를 비활성화하려면 어떻게 해야 할까요? 이를 위해 커스터마이저 객체를 사용하여 구성을 빠르게 할 수 있습니다. 이는 6장에서 폼 로그인 방법에 HTTP 기본 인증을 사용자 정의한 방법과 유사합니다.
이 예제에서는 웹과 보안 종속성만 추가하고 새 프로젝트를 생성한 후, 두 개의 HTTP POST 엔드포인트를 추가하지만, 그중 하나는 CSRF 보호를 사용하지 않도록 제외할 것입니다.
예제: HelloController 클래스 정의
@RestController
public class HelloController {
@PostMapping("/hello") // A
public String postHello() {
return "Post Hello!";
}
@PostMapping("/ciao") // B
public String postCiao() {
return "Post Ciao";
}
}
위의 예제에서 /hello 경로는 CSRF 보호를 받으며, /ciao 경로는 CSRF 토큰 없이 호출할 수 있습니다.
CSRF 보호 커스터마이징
CSRF 보호를 커스터마이징하려면, configuration() 메서드 내에서 HttpSecurity 객체의 csrf() 메서드를 사용하여 Customizer 객체와 함께 사용할 수 있습니다. 아래는 이를 위한 예제 코드입니다.
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(c -> { // A
c.ignoringRequestMatchers("/ciao");
});
http.authorizeRequests(
c -> c.anyRequest().permitAll()
);
return http.build();
}
}
CSRF 토큰 관리
기본적으로 Spring Security는 CSRF 토큰을 서버 측의 HTTP 세션에 저장합니다. 하지만 수평 확장이 필요한 애플리케이션에서는 HTTP 세션을 사용하는 방식이 적합하지 않습니다. 이때, 데이터베이스에 CSRF 토큰을 저장하는 방법을 사용하려면 CsrfToken, CsrfTokenRepository, CsrfTokenRequestHandler와 같은 계약을 구현해야 합니다.
CsrfToken 인터페이스 정의
public interface CsrfToken extends Serializable {
String getHeaderName(); // 기본값: X-CSRF-TOKEN
String getParameterName(); // 기본값: _csrf
String getToken();
}
CsrfTokenRepository 구현
애플리케이션에서 CSRF 토큰을 데이터베이스에 저장하도록 변경하기 위해 CsrfTokenRepository의 사용자 정의 구현을 작성합니다.
@Component
public class CustomCsrfTokenRepository implements CsrfTokenRepository {
private final JpaTokenRepository jpaTokenRepository;
public CustomCsrfTokenRepository(JpaTokenRepository jpaTokenRepository) {
this.jpaTokenRepository = jpaTokenRepository;
}
@Override
public CsrfToken generateToken(HttpServletRequest httpServletRequest) {
String uuid = UUID.randomUUID().toString();
return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", uuid);
}
@Override
public void saveToken(CsrfToken csrfToken,
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) {
String identifier = httpServletRequest.getHeader("X-IDENTIFIER");
Optional<Token> existingToken = jpaTokenRepository.findTokenByIdentifier(identifier);
if (existingToken.isPresent()) {
Token token = existingToken.get();
token.setToken(csrfToken.getToken());
} else {
Token token = new Token();
token.setToken(csrfToken.getToken());
token.setIdentifier(identifier);
jpaTokenRepository.save(token);
}
}
@Override
public CsrfToken loadToken(HttpServletRequest httpServletRequest) {
String identifier = httpServletRequest.getHeader("X-IDENTIFIER");
Optional<Token> existingToken = jpaTokenRepository.findTokenByIdentifier(identifier);
if (existingToken.isPresent()) {
Token token = existingToken.get();
return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token.getToken());
}
return null;
}
}
위 코드에서는 JpaTokenRepository를 사용하여 데이터베이스에서 CSRF 토큰을 저장하고 로드하는 방식으로 구현합니다.
CSRF 보호 설정 클래스
@Configuration
public class ProjectConfig {
private final CustomCsrfTokenRepository customTokenRepository;
public ProjectConfig(CustomCsrfTokenRepository customTokenRepository) {
this.customTokenRepository = customTokenRepository;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(c -> {
c.csrfTokenRepository(customTokenRepository); // A
});
http.authorizeHttpRequests(
c -> c.anyRequest().permitAll()
);
return http.build();
}
}
이 예제에서는 CustomCsrfTokenRepository를 사용하여 CSRF 토큰을 데이터베이스에 저장하고 로드하는 방식으로 CSRF 보호를 구성하고 있습니다.
9.4 요약
- 크로스 사이트 요청 위조(CSRF)는 사용자가 위조된 스크립트가 포함된 페이지에 액세스하도록 속여, 해당 스크립트가 응용 프로그램에 로그인한 사용자를 모방하고 그들을 대신해 동작을 실행하게 만드는 공격입니다.
- CSRF 보호는 Spring Security에서 기본적으로 활성화되어 있습니다.
- Spring Security 아키텍처에서 CSRF 보호 로직의 진입점은 HTTP 필터입니다.
- Spring Security는 사용자가 구현하고 연결하여 사용자 정의 CSRF 보호 기능을 정의할 수 있는 세 가지 간단한 계약을 제공합니다:
- CsrfToken: CSRF 토큰 자체를 설명합니다.
- CsrfTokenRepository: CSRF 토큰을 생성, 저장 및 로드하는 객체를 설명합니다.
- CsrfTokenRequestHandler: 생성된 CSRF 토큰이 HTTP 요청에 설정되는 방식을 관리하는 객체를 설명합니다.
'Spring Security in Action' 카테고리의 다른 글
11장 Implement authorization at the method level (1) | 2024.12.01 |
---|---|
10장 Configuring Cross-Origin Resource Sharing(CORS) (0) | 2024.12.01 |
8장 Configuring endpoint-level authorization: Applying restrictions (0) | 2024.12.01 |
7장 Configuring endpoint-level authorization: Restricting access (0) | 2024.12.01 |
5장 A web app's security begins with filters (0) | 2024.12.01 |