CSRF(Cross Site Request Forgery) Prevention
bindon
2020-02-20
Security Guide
Introduction
CSRF(Cross Site Request Forgery)이란 웹 취약점의 하나로 인터넷 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 요청하게 만드는 공격이다.
Details
사용자의 입력을 받을 때 Form 또는 Ajax를 사용하는 경우 CSRF Token을 추가하여 사용자가 의도하지 않은 요청에 대해 대비해야 한다.
예를 들어 공격자는 사이트의 운영자에게 아래와 같은 이미지 태그가 포함된 메일을 전송한다.
<img src="https://bindon.github.io/changeUserAcoount?id=admin&password=admin" width="0" height="0" />
이 때 만약 운영자가 사이트에 로그인 상태였다고 가정할 때 작성한 URI를 통해 id와 password가 admin으로 변경될 것이며, 운영자는 이미지의 width와 height가 0이기 때문에 자신의 패스워드가 변경되었다는 것을 눈치채지 못하고 공격자가 접속할 때 까지 조치하지 못 할 것이다. 이러한 공격을 방어하기 위해 CSRF Token을 사용하면, 공격자는 CSRF Token을 생성할 수 없기 때문에 매개변수로 만들어 전달할 수 없게 된다. 서버측에서는 CSRF Token이 같이 전송되지 않았기 때문에 해당 요청을 거절하게 된다. 만약 GET으로 토큰을 받은 후 POST로 전송하는 케이스가 아닌 경우 CSRF 토큰을 받을 수 없기 때문에, CSRF 토큰을 조회하는 API를 만들어 사용자의 정보를 입력하여 전송하기 전 CSRF 토큰을 받아 동일한 방식으로 서버에서 점검할 수 있도록 조치해야 한다.
CSRF Prevention Technique
Token Based Mitigation
- State: synchronizer token pattern
- 현재 가장 많이 사용하고 있으며 가장 권장하는 방법
- 요청 당 한 개의 토큰 발급을 권장하지만, 필요에 따라 세션 당 한 개의 토큰 발급을 수행할 수 있음
- Stateless: encrypted/hash based token pattern
Synchronizer Token Pattern
- CSRF 공격을 방지하기 위한 CSRF Token의 조건
- 사용자 세션마다 고유한 CSRF Token 생성
- 충분히 큰 Random 값: 32 bytes == 256 bits 권장
- CSPRNG(Cryptographically Secure Pseudo-Random Number Generator) 사용: Java의 SecureRandom
- Hidden Field, HTTP Header에 추가할 수 있으며, Form 및 Ajax에 사용될 수 있음
- 보안성: HTTP Header > Hidden Field, 공격자가 XMLHttpRequest 헤더를 스푸핑하려고 할 때 브라우저의 CORS로 인하여 방지할 수 있음
- 높은 보안성이 요구될 때에는 하나의 요청 당 하나의 CSRF Token을 생성해야 하지만, 세션당 한 개의 CSRF Token을 발행할 수 있음
Encrypted Based Token Pattern
- 서버에서 가지고 있는 키를 이용하여 Session ID와 Timestamp(for replay attack 방지)로 구성된 토큰을 생성하여 전달
- AES256-GCM 권장
- 클라이언트에서는 Hidden Field 또는 HTTP Header에 추가할 수 있으며, From 및 Ajax에 사용될 수 있음
- 서버측에서 값을 다시 받아 자신의 키를 이용하여 복호화를 수행하고 검증을 수행
- Session ID로 사용자를 검증
- Timestamp로 만료 시간을 검증
HMAC Based Token Pattern
- 서버에서 가지고 있는 키를 이용하여 토큰을 생성
- CSRF Token = HMAC(sessionId + timestamp) || timestamp
- 클라이언트에서는 Hidden Field 또는 HTTP Header에 추가할 수 있으며, From 및 Ajax에 사용될 수 있음
- 서버측에서 값을 다시 받아 자신의 키를 이용하여 복호화를 수행하고 검증을 수행
- CSRF Token에서 timestamp를 떼어내고 만료 시간을 검증
- 유효한 시간 내로 요청이 왔다면, sessionId에 떼어낸 timestamp를 붙여 HMAC 생성
- HMAC이 동일한지 확인
Defense In Depth Techniques
- Set-Cookie의 SameSite 사용
- SameSite=Strict를 적용
- 사이트 간 요청과 함께 쿠키를 보낼지 여부를 결정
Double-Submit Cookie
- 서버에서 CSRF Token의 상태를 유지할 수 없을 경우 사용
- 동작 방식(매 요청 시 마다 생성)
- 서버에서 CSRF Token을 생성하여 Cookie(필수) 및 Parameter(선택)로 내려줌
- SameSite, Secure 속성 적용
- 클라이언트에서는 Hidden Field 또는 HTTP Header에 추가할 수 있으며, From 및 Ajax에 사용될 수 있음
- 서버측에서 올라온 Cookie의 CSRF Token과 Form Parameter 또는 HTTP Header에 전송된 CSRF Token이 일치하는지 확인
- 만약 다른 도메인에서 공격을 수행할 경우 CORS로 인해 Cookie에 접근할 수 없을 뿐만 아니라 Cookie 자체도 전송할 수 없음
Examples
1. Generate CSRF Token in Back End
Spring(Spring Security Compatible)
/** CSRF Token Name */
private static final String CSRF_TOKEN_MAP_KEY = "_csrf";
private static final String CSRF_TOKEN_PARAMETER_NAME = "_csrf";
private static final String CSRF_TOKEN_HEADER_NAME = "X-CSRF-TOKEN";
// FIXME : REPOSITORY and USER_SESSION is example. DO NOT USE
/** @deprecated */
private static final Map<String, String> REPOSITORY = new HashMap<String, String>();
/** @deprecated */
private static final String USER_SESSION = "USER_SESSION";
@GetMapping("/csrf")
public ModelAndView getCsrfPage() {
ModelAndView csrfModel = new ModelAndView("csrf");
// Generate CSRF Token
String currentUUID = UUID.randomUUID().toString();
Map<String, String> csrfTokenMap = Map.of(
"parameterName", CSRF_TOKEN_PARAMETER_NAME,
"headerName", CSRF_TOKEN_HEADER_NAME,
"token", currentUUID);
// FIXME : Map CSRF token and user identification data and store it in repository
REPOSITORY.put(USER_SESSION, currentUUID);
// Allocate csrfToken
csrfModel.addObject(CSRF_TOKEN_MAP_KEY, csrfTokenMap);
return csrfModel;
}
Spring Security(Default: CSRF Enabled)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf();
}
}
2. Receive CSRF Token and Send it in Front End
HTML Form
<form method="post">
<div class="form-group">
<div>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
<input type="text" name="value" id="value" value="bindon" />
<input type="submit" class="btn btn-primary" id="submit" value="Send using form" />
<input type="button" class="btn btn-primary" id="button" value="Send using ajax" />
</div>
</div>
</form>
Ajax
$.ajax({
url: "/csrf",
type: "post",
headers: {
"${_csrf.headerName}": "${_csrf.token}"
},
data: {
"value": $("#value").val()
},
success: function(response) {
alert(response);
}
});
3. Verify CSRF Token and Exception Handling in Back End
Spring
@PostMapping("/csrf")
@ResponseBody
public String checkCsrfToken(
@RequestHeader(value=CSRF_TOKEN_HEADER_NAME, required=false) String csrfTokenInHeader,
@RequestParam (value=CSRF_TOKEN_PARAMETER_NAME, required=false) String csrfTokenInParameter,
@RequestParam("value") String value) {
String result = "Failed";
// Compare csrf tokens(server and client)
if(!REPOSITORY.get(USER_SESSION).equals(csrfTokenInHeader)
&& !REPOSITORY.get(USER_SESSION).equals(csrfTokenInParameter)) {
// TODO : Exception logic(Invalid token)
throw new SecurityException("CSRF Token is Invalid");
}
// TODO : Business Logic
result = value;
return result;
}
Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.accessDeniedPage("/")
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
if(accessDeniedException instanceof MissingCsrfTokenException) {
// handle missing CSRF Token case
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
} else if(accessDeniedException instanceof InvalidCsrfTokenException) {
// handle invalid CSRF Token case
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
}).and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
}
Security Guide Post List
TITLE | DATE | File Upload | 2020-02-26 | API Security | 2020-02-26 | HTTP Security Headers | 2020-02-21 | CSRF(Cross Site Request Forgery) Prevention | 2020-02-20 | XSS(Cross-Site Scripting) Prevention | 2020-02-17 | AES-GCM Example in Java | 2019-11-28 | ECDH Example in Java | 2019-11-25 |
---|