크로스 사이트 요청 위조(CSRF, Cross-Site Request Forgery)는 웹 애플리케이션 보안 취약점 중 하나로, 사용자가 인증된 상태에서 악의적인 사이트를 방문했을 때, 사용자의 의지와는 상관없이 공격자가 의도한 요청을 해당 웹 애플리케이션에 보내게 만드는 공격 기법입니다.
사용자가 웹 애플리케이션에 로그인하면, 세션 쿠키나 인증 토큰을 통해 인증 상태가 유지 -> 사용자가 악성 웹사이트나 공격자가 조작한 웹 페이지를 방문합니다. -> 해당 페이지는 백그라운드에서 원래 웹 애플리케이션으로 인증된 요청을 자동으로 생성 -> 서버는 이 요청을 합법적인 사용자로부터 온 것으로 인식하고, 요청을 처리하게 됨
ex) 사용자가 은행 웹사이트에 로그인한 상태에서 악성 웹사이트를 방문하게 되면, 공격자는 사용자의 은행 계좌에서 돈을 이체하는 요청을 자동으로 생성할 수 있습니다.
Spring Framework를 사용한 Java/Spring 백엔드 개발자라면, Spring Security를 통해 CSRF 공격을 방지할 수 있습니다.
1. Spring Security를 통해 CSRF 공격을 방지할 수 있습니다. Spring Security는 기본적으로 CSRF 보호 기능을 제공하며, 이를 적절히 설정하는 것이 중요합니다.
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
<form th:action="@{/submit}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<!-- 기타 폼 필드 -->
<button type="submit">제출</button>
</form>
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
fetch('/api/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[csrfHeader]: csrfToken
},
body: JSON.stringify({ /* 데이터 */ })
});
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
public class CsrfConfig {
@Bean
public CookieCsrfTokenRepository csrfTokenRepository() {
CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
repository.setCookiePath("/");
repository.setCookieSameSite("Strict");
return repository;
}
}
물론입니다! 취업 준비 중인 신입 Java/Spring 백엔드 개발자로서 CSRF(Cross-Site Request Forgery)에 대한 이론을 실습을 통해 깊이 있게 이해하는 것은 매우 중요합니다. 아래에 단계별로 따라할 수 있는 실습 예제를 제공하겠습니다. 이 실습을 통해 CSRF 공격의 개념을 이해하고, Spring Security를 사용하여 이를 방어하는 방법을 직접 구현해 볼 수 있습니다.
먼저, Spring Boot 프로젝트를 생성하고 기본적인 사용자 인증 기능을 설정합니다.
src
├── main
│ ├── java
│ │ └── com.example.csrf
│ │ ├── CsrfDemoApplication.java
│ │ ├── config
│ │ │ └── SecurityConfig.java
│ │ └── controller
│ │ └── HomeController.java
│ └── resources
│ ├── templates
│ │ ├── home.html
│ │ ├── login.html
│ │ └── transfer.html
│ └── application.properties
CsrfDemoApplication.java
package com.example.csrf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CsrfDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CsrfDemoApplication.class, args);
}
}
application.properties
spring.thymeleaf.cache=false
server.port=8080
Spring Security를 설정하여 간단한 로그인 기능을 구현합니다.
package com.example.csrf.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 인메모리 사용자 설정 (실습용)
auth.inMemoryAuthentication()
.withUser("user")
.password("{noop}password") // {noop}는 암호 인코더를 사용하지 않음을 의미
.roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/css/**").permitAll() // 로그인 페이지는 모두 접근 가능
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home", true)
.permitAll()
.and()
.logout()
.permitAll();
// CSRF 보호는 기본적으로 활성화되어 있음
}
}
package com.example.csrf.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/login")
public String login() {
return "login"; // login.html 템플릿 반환
}
@GetMapping("/home")
public String home() {
return "home"; // home.html 템플릿 반환
}
@GetMapping("/transfer")
public String transfer() {
return "transfer"; // transfer.html 템플릿 반환
}
}
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<h2>Login</h2>
<form th:action="@{/login}" method="post">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</body>
</html>
home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
</head>
<body>
<h2>Welcome Home!</h2>
<a th:href="@{/transfer}">Transfer Money</a>
<form th:action="@{/logout}" method="post">
<button type="submit">Logout</button>
</form>
</body>
</html>
transfer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Transfer Money</title>
</head>
<body>
<h2>Transfer Money</h2>
<form th:action="@{/transfer}" method="post">
<div>
<label>Amount:</label>
<input type="text" name="amount"/>
</div>
<div>
<button type="submit">Transfer</button>
</div>
</form>
</body>
</html>
이 단계에서는 CSRF 보호가 기본적으로 활성화된 상태에서 이를 비활성화하고 공격을 시뮬레이션해봅니다.
SecurityConfig.java 파일에서 CSRF 보호를 비활성화합니다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/css/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home", true)
.permitAll()
.and()
.logout()
.permitAll()
.and()
.csrf().disable(); // CSRF 보호 비활성화
}
프로젝트를 실행하고 http://localhost:8080/login
에 접속하여 사용자(user
/password
)로 로그인합니다. 로그인 후 /home
페이지로 이동하게 됩니다.
CSRF 공격을 시뮬레이션하기 위해 다른 웹 페이지를 만들어 사용자가 /transfer
엔드포인트로 POST 요청을 보내도록 합니다.
malicious.html (공격자 사이트)
<!DOCTYPE html>
<html>
<head>
<title>Malicious Site</title>
</head>
<body>
<h2>Malicious Action</h2>
<form action="http://localhost:8080/transfer" method="POST">
<input type="hidden" name="amount" value="1000"/>
<button type="submit">Click me!</button>
</form>
<script>
// 자동으로 폼 제출
window.onload = function() {
document.forms[0].submit();
};
</script>
</body>
</html>
공격 시나리오:
/transfer
페이지에 로그인된 상태에서 악성 사이트(malicious.html
)를 방문합니다./transfer
엔드포인트로 전송하여 사용자의 권한으로 금액을 이체합니다.주의: 실제 환경에서는 이러한 공격이 심각한 보안 문제를 야기할 수 있으므로, 이 실습은 로컬 개발 환경에서만 수행하시기 바랍니다.
CSRF 보호를 다시 활성화하고 보호 메커니즘을 이해합니다.
SecurityConfig.java 파일에서 CSRF 보호를 다시 활성화합니다. 기본 설정으로 돌아가면 됩니다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/css/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home", true)
.permitAll()
.and()
.logout()
.permitAll();
// .csrf()는 기본적으로 활성화되어 있음
}
/transfer
엔드포인트가 POST 요청을 처리하도록 컨트롤러를 수정합니다.
HomeController.java
@PostMapping("/transfer")
public String transfer(@RequestParam("amount") String amount, Model model) {
// 간단한 이체 로직 (실습용)
model.addAttribute("message", "Transferred " + amount + " successfully!");
return "home";
}
Thymeleaf 템플릿을 사용하여 CSRF 토큰을 폼에 자동으로 포함시킵니다.
transfer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Transfer Money</title>
</head>
<body>
<h2>Transfer Money</h2>
<form th:action="@{/transfer}" method="post">
<div>
<label>Amount:</label>
<input type="text" name="amount"/>
</div>
<div>
<button type="submit">Transfer</button>
</div>
<!-- CSRF 토큰 자동 추가 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</form>
</body>
</html>
Thymeleaf는 Spring Security와 통합되어 있어, _csrf
변수를 사용할 수 있습니다. 이를 통해 CSRF 토큰이 폼에 포함됩니다.
CSRF 보호가 제대로 작동하는지 확인해봅니다.
프로젝트를 다시 실행하고, http://localhost:8080/login
에 접속하여 사용자로 로그인합니다.
앞서 만든 malicious.html
을 다시 사용하여 공격을 시도합니다. 하지만 이번에는 CSRF 보호가 활성화되어 있기 때문에 공격이 실패해야 합니다.
예상 결과:
transfer.html
의 폼에 CSRF 토큰이 포함되어 있는 것을 확인할 수 있습니다. 공격자가 이 토큰을 알지 못하면 요청을 성공적으로 보낼 수 없습니다.
좀 더 심화된 실습으로, AJAX 요청에 CSRF 토큰을 포함시키는 방법을 구현해봅니다.
transfer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Transfer Money</title>
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
</head>
<body>
<h2>Transfer Money</h2>
<input type="text" id="amount" placeholder="Amount"/>
<button onclick="transferMoney()">Transfer</button>
<div id="result"></div>
<script>
function transferMoney() {
const amount = document.getElementById('amount').value;
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
fetch('/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[csrfHeader]: csrfToken
},
body: JSON.stringify({ amount: amount })
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Network response was not ok.');
})
.then(data => {
document.getElementById('result').innerText = data.message;
})
.catch(error => {
document.getElementById('result').innerText = 'Transfer failed.';
});
}
</script>
</body>
</html>
HomeController.java
import org.springframework.http.ResponseEntity;
import java.util.HashMap;
import java.util.Map;
// ...
@PostMapping("/transfer")
public ResponseEntity<Map<String, String>> transfer(@RequestBody Map<String, String> payload) {
String amount = payload.get("amount");
// 간단한 이체 로직 (실습용)
Map<String, String> response = new HashMap<>();
response.put("message", "Transferred " + amount + " successfully!");
return ResponseEntity.ok(response);
}
Spring Security는 기본적으로 CSRF 토큰을 HTTP 헤더에 포함하도록 설정되어 있습니다. 위의 JavaScript 코드는 이를 반영하여 X-CSRF-TOKEN
헤더에 토큰을 포함시킵니다.
프로젝트를 실행하고, /transfer
페이지에서 AJAX를 통해 이체를 시도해봅니다. CSRF 토큰이 제대로 포함되어 있다면 요청이 성공하고, 그렇지 않다면 거부됩니다.
OWASP WebGoat 사용: OWASP에서 제공하는 WebGoat 프로젝트는 다양한 웹 보안 취약점을 실습할 수 있는 교육용 애플리케이션입니다. CSRF 관련 실습도 포함되어 있으니 참고해보세요. WebGoat GitHub
Postman을 활용한 테스트: Postman과 같은 API 테스트 도구를 사용하여 CSRF 토큰을 포함하지 않은 요청을 시도해보고, Spring Security가 이를 어떻게 처리하는지 확인해보세요.
SameSite 쿠키 설정 실습: SameSite
쿠키 속성을 설정하여 CSRF 공격을 추가로 방어하는 방법을 실습해보세요. 이는 CSRF 보호를 강화하는 좋은 방법입니다.
실제 프로젝트에 적용: 개인 프로젝트나 학습 프로젝트에 CSRF 보호를 적용하고, 다양한 공격 시나리오를 시도해보며 이해를 깊이 있게 해보세요.
이번 실습을 통해 CSRF의 개념을 이해하고, Spring Security를 사용하여 이를 효과적으로 방어하는 방법을 배웠습니다. 실제 프로젝트에서 이러한 보안 메커니즘을 올바르게 구현하는 것은 매우 중요하며, 이는 신입 개발자로서의 역량을 크게 향상시킬 것입니다. 추가적인 보안 주제도 학습하여 더욱 안전한 웹 애플리케이션을 개발할 수 있도록 노력해보세요!