CSRF
CSRF(Cross Site Request Forgery, 크로스 사이트 요청 위조)는 웹 보안 취약점 중 하나로, 인증된 사용자가 자신의 의지와는 무관하게 웹 애플리케이션에 공격자가 의도한 특정 요청을 보내도록 유도하는 것을 말한다.
제품 구입, 자금 이체, 비밀번호 변경, 기록 삭제 등의 요청을 악의적으로 보내는 것이다.
CSRF 공격 예제
CSRF 공격을 이해하기 위해 Spring Docs에 좋은 예제가 있어서 가져왔다.
https://docs.spring.io/spring-security/reference/features/exploits/csrf.html#csrf
(실제로는 여러 가지 보안 제한사항이 있겠지만, 그런 게 없다고 가정하자.)
정상적인 요청 <form>
은행 웹 사이트에 로그인 사용자가 다른 계좌로 자금을 이체할 수 있는 <form>이 있다고 가정해 보자.
<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>
이 from을 작성하여 요청을 보내면 아래와 같은 HTTP Request가 나갈 것이다.
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 공격 상황
은행 웹사이트를 로그인하여 이용하고, 인증된 상태로 로그아웃을 하지 않고 악의적인 웹사이트를 방문했다고 가정해 보자. 악의적인 웹사이트에는 아래와 같은 HTML 페이지가 포함되어 있다.
악의적인 공격 <form>
<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>
사용자에게 해당 form이 담긴 html을 클릭하게 만든다.
공격자가 지정한 악의적인 값을 담은 form이 사용자의 인증 정보와 함께 POST 요청이 나간다.
bank.example.com에서는 해당 사용자가 보낸 요청으로 생각하고, 자금을 이체하는 처리를 할 것이다.
이 공격이 가능한 이유는 정상적인 HTTP Request와 공격자의 HTTP Request가 완전히 동일하기 때문이다.
따라서 이 요청이 정상 사용자가 보낸 요청이라는 것을 확인하기 위한 무언가가 필요하다.
CSRF 공격 보호 예제
Synchronizer Token Pattern
동기화 토큰 패턴은 사용자 세션 또는 요청 단위로 토큰을 만들어서, 서버에서 제공하는 방식이다.
클라이언트는 이 토큰을 요청에 추가해서 보내고, 서버에서 다시 토큰을 제공받기를 반복한다.
서버는 클라이언트의 요청에서 csrf 토큰의 존재 여부를 확인하고, 유효성을 검증하고, 서버 측 사용자 세션에 저장된 토큰 값과 비교하는 과정을 거친다. 클라이언트가 보낸 csrf 토큰과 서버 측 세션의 토큰이 일치한다면, 정당한 사용자가 보낸 요청으로 간주할 수 있다.
사용자 로그인 시 서버는 csrf 토큰을 생성해서 사용자 세션이 할당하고, 요청 시에 아래와 같이 csrf 값을 포함해서 전송한다.
<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 요청에 _csrf 값이 추가되어 나가게 되고, 이 값을 확인하는 과정을 거쳐서 보안을 강화한다.
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토큰을 계속해서 갱신할 수도 있다. 요청 시마다 CSRF를 받아서 확인한 뒤, 새로운 토큰을 생성해서 서버 측 세션에 저장하고 응답에 담아서 보내고, 클라이언트 측에서도 토큰을 받아서 갱신하는 방법도 있다.
SpringSecurity CSRF 보안
Spring Security DOCS
https://docs.spring.io/spring-security/reference/features/exploits/csrf.html#csrf
Spring Security에서는 간단한 설정으로 CSRF 보안 기능을 추가할 수 있다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf(Customizer.withDefaults());
return http.build();
}
}
SecurityFilterChain에 위와 같은 간단한 코드를 추가하면 위에서 설명한 복잡한 과정을 자동으로 처리해 준다.
이건 뭐 그냥 쓰면 될 것 같고, 내부 구조를 알아보자.
CSRF Protection Components
CsrfFilter 동작 과정
- ① DeferredCsrfToken을 CsrfTokenRepository에 저장한다. (이후 ④에서 CSRF 토큰을 불러올 때 사용한다.)
- ② Supplier<CsrfToken>을 CsrfTokenRequestHandler로 전달하여 CSRF 토큰 관련 속성에 할당하고, 애플리케이션의 다른 부분에서 토큰을 사용할 수 있도록 한다.
- ③ CSRF 보호 처리가 시작되고, 현재 요청이 CSRF 보호가 필요한 지 확인하고, 필요하지 않다면 Filter Chain을 종료한다.
- ④ CSRF 보호가 필요한 경우 ①에서 저장해 둔 CSRF 토큰을 CsrfTokenRepository에서 불러와서 로드된다.
(클라이언트가 보내준 토큰과 비교하기 위해 이전에 저장해 둔 토큰을 꺼내오는 것이다.) - ⑤ 클라이언트의 요청으로부터 CsrfTokenRequestHandler가 CSRF 토큰을 추출한다.
- ⑥ 토큰의 유효성을 검사한다. ( ④에서 가져온 토큰과 ⑤에서 가져온 토큰을 비교한다.)
- ⑦ 토큰 유효성 검사 실패 시 AccessDeniedException을 발생시켜서 AccessDeniedHandler에 전달되고 처리 과정을 종료한다. (권한 없는 접근에 대한 요청 거부이다.)
- 유효성 검사 성공(⑥) 시 정상적으로 FilterChain을 종료한다.
DefaultCsrfToken
public DefaultCsrfToken(String headerName,String parameterName,String token)
HTTP header name, HTTP parameter name, Token Value로 구성된 간단한 클래스이다. 무작위의 String token 값이 중요하다. 위에서 Supplier로 감싸는 이유는, 지연 로딩(Lazy Loading)을 위해서라고 한다. 실제 CSRF 토큰이 필요할 때까지 생성하거나 메모리에 로드하지 않고 기다렸다가, 필요할 때 생성해서 전달하기 위함이다. (성능 향상과 보안을 위함)
정리
정리하자면, CSRF 공격은 사용자가 의도하지 않은 요청을 마치 사용자가 보낸 것처럼 사이트에 요청을 보내는 것이다.
이를 방지하기 위해 CSRF 토큰을 사용하며, 사용자 인증시에 토큰을 하나 발행하여 클라이언트 측, 서버 측 세션에 각각 저장해 뒀다가 HTTP 요청마다 비교하는 것이다.
'[ Computer Science ] > Web & Network' 카테고리의 다른 글
[Network] 포트(PORT), 소켓(Socket)이란? (웹소켓 아님 Web Socket != Socket) (0) | 2024.08.07 |
---|---|
[Network] 방화벽 - 인바운드 & 아웃바운드 규칙이란? (0) | 2024.08.06 |
[Network] CORS란? Cross-Origin Resource Sharing / CORS 에러, Spring Boot (0) | 2024.07.29 |
[Network] OSI 7계층과 TCP/IP 4계층 (0) | 2023.07.19 |
[Web/Network] REST API란? (0) | 2023.07.19 |