
Cross-Site Request Forgery (CSRF)는 사용자가 신뢰하는 웹 애플리케이션에서 사용자 모르게 명령을 실행하게 만드는 공격이다. 사용자가 의도하지 않은 웹 요청을 제출하도록 속인다.
CSRF 공격은 사용자의 브라우저가 특정 웹사이트에 신뢰된 요청을 보내도록 만들어 세션 상태 변경, 계정 조작 등의 행위를 수행할 수 있다.
예를 들어, 은행 웹 사이트가 현재 로그인된 사용자가 다른 은행으로 돈을 이체할 수 있는 폼을 제공한다고 가정하자.
이체 폼
<form method="post" action="/transfer">
<input type="text" name="amount"/>
<input type="text" name="routingNumber"/>
<input type="text" name="account"/>
<input type="submit" value="Transfer"/>
</form>
이체 HTTP 요청
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
이제 은행 웹 사이트에 인증한 후 로그아웃하지 않고 악성 웹 사이트를 방문했다고 가정한다.
악성 웹 사이트에는 다음과 같은 폼이 포함된 HTML 페이지가 있다.
<form method="post" action="https://bank.example.com/transfer">
<input type="hidden" name="amount" value="100.00"/>
<input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
<input type="hidden" name="account" value="evilsAccountNumber"/>
<input type="submit" value="Win Money!"/>
</form>
버튼을 클릭하면, 악성 사용자에게 100 달러를 이체하게 된다. 악성 웹 사이트가 쿠키를 볼 수는 없지만, 은행과 관련된 쿠키가 요청과 함께 전송되기 때문이다.
더 심각한 문제는 이 과정이 자바스크립트를 사용해 자동화될 수 있다는 점이다. 사용자가 버튼을 클릭할 필요도 없다는 것이다. 원래는 정상적인 웹 사이트였지만, XSS 공격을 받아 취약점이 생긴 사이트를 방문해도 이런 일이 생길 수 있다.
CSRF 공격이 가능한 이유는 피해자의 웹사이트와 공격자의 웹사이트에서 보낸 HTTP 요청이 정확히 동일하기 때문이다. CSRF 공격을 방어하기 위해서는 악성 사이트가 제공할 수 없는 무언가가 요청에 포함되어 있어야 두 요청을 구별할 수 있다.
동기화 토큰 패턴
CSRF 공격으로부터 보호하는 가장 일반적이고 종합적인 방법은 동기화 토큰 패턴을 사용하는 것이다. 세션 쿠키와 더불어 각 HTTP 요청에 보안 무작위 생성 값을 요구하는 것이다. 이 값을 CSRF 토큰이라고 한다.
HTTP 요청이 제출되면 서버는 예상되는 CSRF 토큰을 조회하고 HTTP 요청의 실제 CSRF 토큰과 비교해야 한다. 값이 일치하지 않으면 HTTP 요청을 거부해야 한다.
HTTP 매개변수 또는 HTTP 헤더에 실제 CSRF 토큰이 포함되도록 요구하면 CSRF 공격으로부터 보호할 수 있다. 쿠키에 CSRF 토큰을 포함시키는 것은 브라우저가 자동으로 쿠키를 HTTP 요청에 포함시키기 때문에 작동하지 않는다.
<form method="post" action="/transfer">
<input type="hidden" name="_csrf" value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text" name="amount"/>
<input type="text" name="routingNumber"/>
<input type="hidden" name="account"/>
<input type="submit" value="Transfer"/>
</form>
이 폼의 HTTP 요청은 다음과 같다.
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
악성 웹사이트는 _csrf 매개변수의 올바른 값을 제공할 수 없어 서버가 실제 CSRF 토큰을 예상된 CSRF 토큰과 비교할 때 이체가 실패한다.
SameSite 속성
서버는 쿠키를 설정할 때 SameSite 속성을 지정하여 외부 사이트에서 요청이 올 때 쿠키를 전송하지 않도록 할 수 있다.
Spring Security는 세션 쿠키의 생성을 직접 제어하지 않으므로 SameSite 속성에 대한 지원을 제공하지 않는다. Spring Session은 서블릿 기반 애플리케이션에서 SameSite 속성을 지원한다. Spring Framework의 CookieWebSessionIdResolver는 WebFlux 기반 애플리케이션에서 SameSite 속성에 대한 기본 지원을 제공한다.
SameSite 속성을 사용하여 CSRF 공격을 방어할 때 몇 가지 중요한 고려 사항이 있다.
SameSite 속성을 Strict로 설정하면 강력한 방어를 제공하지만 사용자를 혼란스럽게 할 수 있다. 예를 들어, 사용자가 social.example.com에 로그인된 상태에서 email.example.org에서 이메일을 받고 이메일에 포함된 링크를 클릭하면, 사용자는 social.example.com에 인증된 상태로 이동할 것으로 기대한다. 그러나 SameSite 속성이 Strict로 설정된 경우 쿠키가 전송되지 않아 사용자가 인증되지 않는다.
Spring Security는 POST 요청과 같은 안전하지 않은 HTTP 메서드에 대해 기본적으로 CSRF 공격으로부터 보호하므로 추가 코드는 필요하지 않지만, 기본 구성을 명시적으로 지정할 수 있다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf(Customizer.withDefaults());
return http.build();
}
}
Spring Security의 CSRF 처리 단계를 나타낸 그림이다.
