크로스 사이트 요청 위조(CSRF)란 무엇인가요?

김상욱·2024년 12월 31일
0

크로스 사이트 요청 위조(CSRF)란 무엇인가요?

크로스 사이트 요청 위조(CSRF, Cross-Site Request Forgery)는 웹 애플리케이션 보안 취약점 중 하나로, 사용자가 인증된 상태에서 악의적인 사이트를 방문했을 때, 사용자의 의지와는 상관없이 공격자가 의도한 요청을 해당 웹 애플리케이션에 보내게 만드는 공격 기법입니다.

사용자가 웹 애플리케이션에 로그인하면, 세션 쿠키나 인증 토큰을 통해 인증 상태가 유지 -> 사용자가 악성 웹사이트나 공격자가 조작한 웹 페이지를 방문합니다. -> 해당 페이지는 백그라운드에서 원래 웹 애플리케이션으로 인증된 요청을 자동으로 생성 -> 서버는 이 요청을 합법적인 사용자로부터 온 것으로 인식하고, 요청을 처리하게 됨

ex) 사용자가 은행 웹사이트에 로그인한 상태에서 악성 웹사이트를 방문하게 되면, 공격자는 사용자의 은행 계좌에서 돈을 이체하는 요청을 자동으로 생성할 수 있습니다.

CSRF 방지 방법

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();
    }
}
  1. CSRF 토큰 사용: 클라이언트(예: 웹 브라우저)에서 서버로 요청을 보낼 때, CSRF 토큰을 포함시켜야 합니다. 이는 요청이 합법적인 출처에서 온 것인지 검증하는 역할을 합니다.
  • HTML 폼에 CSRF 토큰 추가 : Thymeleaf와 같은 템플릿 엔진을 사용하는 경우, 다음과 같이 CSRF 토큰을 폼에 포함시킬 수 있습니다.
<form th:action="@{/submit}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    <!-- 기타 폼 필드 -->
    <button type="submit">제출</button>
</form>
  • AJAX 요청에 CSRF 토큰 추가 : JavaScript를 사용해 AJAX 요청을 보낼 때, CSRF 토큰을 헤더에 포함시켜야 합니다.
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({ /* 데이터 */ })
});
  1. SameSite 쿠키 속성 사용: 쿠키에 SameSite 속성을 설정하여, 다른 사이트에서 쿠키가 전송되지 않도록 제한할 수 있습니다. 이는 CSRF 공격을 완화하는 데 도움이 됩니다.
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를 사용하여 이를 방어하는 방법을 직접 구현해 볼 수 있습니다.

실습 개요

  1. 간단한 Spring Boot 애플리케이션 설정
  2. 사용자 인증 및 권한 설정
  3. CSRF 보호 미적용 상태에서 CSRF 공격 시뮬레이션
  4. Spring Security를 사용하여 CSRF 보호 적용
  5. CSRF 보호가 적용된 상태에서 공격 시도 및 방어 확인
  6. CSRF 토큰을 사용하는 폼과 AJAX 요청 구현

1. 간단한 Spring Boot 애플리케이션 설정

먼저, Spring Boot 프로젝트를 생성하고 기본적인 사용자 인증 기능을 설정합니다.

1.1. 프로젝트 생성

  • Spring Initializr(https://start.spring.io/)를 사용하여 새로운 Spring Boot 프로젝트를 생성합니다.
  • Dependencies:
    • Spring Web
    • Spring Security
    • Thymeleaf (템플릿 엔진)
    • Spring Boot DevTools (개발 편의를 위해)

1.2. 프로젝트 구조

src
├── main
│   ├── java
│   │   └── com.example.csrf
│   │       ├── CsrfDemoApplication.java
│   │       ├── config
│   │       │   └── SecurityConfig.java
│   │       └── controller
│   │           └── HomeController.java
│   └── resources
│       ├── templates
│       │   ├── home.html
│       │   ├── login.html
│       │   └── transfer.html
│       └── application.properties

1.3. 기본 설정

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

2. 사용자 인증 및 권한 설정

Spring Security를 설정하여 간단한 로그인 기능을 구현합니다.

2.1. SecurityConfig.java

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 보호는 기본적으로 활성화되어 있음
    }
}

2.2. HomeController.java

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 템플릿 반환
    }
}

2.3. 템플릿 생성

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>

3. CSRF 보호 미적용 상태에서 CSRF 공격 시뮬레이션

이 단계에서는 CSRF 보호가 기본적으로 활성화된 상태에서 이를 비활성화하고 공격을 시뮬레이션해봅니다.

3.1. 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 보호 비활성화
}

3.2. 애플리케이션 실행

프로젝트를 실행하고 http://localhost:8080/login에 접속하여 사용자(user/password)로 로그인합니다. 로그인 후 /home 페이지로 이동하게 됩니다.

3.3. CSRF 공격 시뮬레이션

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>

공격 시나리오:

  1. 사용자가 /transfer 페이지에 로그인된 상태에서 악성 사이트(malicious.html)를 방문합니다.
  2. 악성 사이트는 자동으로 POST 요청을 /transfer 엔드포인트로 전송하여 사용자의 권한으로 금액을 이체합니다.

주의: 실제 환경에서는 이러한 공격이 심각한 보안 문제를 야기할 수 있으므로, 이 실습은 로컬 개발 환경에서만 수행하시기 바랍니다.


4. Spring Security를 사용하여 CSRF 보호 적용

CSRF 보호를 다시 활성화하고 보호 메커니즘을 이해합니다.

4.1. SecurityConfig.java 수정

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()는 기본적으로 활성화되어 있음
}

4.2. transfer 엔드포인트 구현

/transfer 엔드포인트가 POST 요청을 처리하도록 컨트롤러를 수정합니다.

HomeController.java

@PostMapping("/transfer")
public String transfer(@RequestParam("amount") String amount, Model model) {
    // 간단한 이체 로직 (실습용)
    model.addAttribute("message", "Transferred " + amount + " successfully!");
    return "home";
}

4.3. CSRF 토큰 추가

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 토큰이 폼에 포함됩니다.


5. CSRF 보호가 적용된 상태에서 공격 시도 및 방어 확인

CSRF 보호가 제대로 작동하는지 확인해봅니다.

5.1. 애플리케이션 실행

프로젝트를 다시 실행하고, http://localhost:8080/login에 접속하여 사용자로 로그인합니다.

5.2. CSRF 공격 시도

앞서 만든 malicious.html을 다시 사용하여 공격을 시도합니다. 하지만 이번에는 CSRF 보호가 활성화되어 있기 때문에 공격이 실패해야 합니다.

예상 결과:

  • 공격자가 전송한 POST 요청에는 유효한 CSRF 토큰이 포함되지 않았으므로, Spring Security는 요청을 거부합니다.
  • 사용자의 금액 이체가 이루어지지 않습니다.

5.3. 공격 결과 확인

transfer.html의 폼에 CSRF 토큰이 포함되어 있는 것을 확인할 수 있습니다. 공격자가 이 토큰을 알지 못하면 요청을 성공적으로 보낼 수 없습니다.


6. CSRF 토큰을 사용하는 폼과 AJAX 요청 구현

좀 더 심화된 실습으로, AJAX 요청에 CSRF 토큰을 포함시키는 방법을 구현해봅니다.

6.1. transfer.html 수정 (AJAX 사용)

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>

6.2. Controller 수정 (JSON 응답)

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);
}

6.3. CSRF 토큰 헤더 설정

Spring Security는 기본적으로 CSRF 토큰을 HTTP 헤더에 포함하도록 설정되어 있습니다. 위의 JavaScript 코드는 이를 반영하여 X-CSRF-TOKEN 헤더에 토큰을 포함시킵니다.

6.4. 애플리케이션 실행 및 테스트

프로젝트를 실행하고, /transfer 페이지에서 AJAX를 통해 이체를 시도해봅니다. CSRF 토큰이 제대로 포함되어 있다면 요청이 성공하고, 그렇지 않다면 거부됩니다.


추가 팁 및 권장 사항

  1. OWASP WebGoat 사용: OWASP에서 제공하는 WebGoat 프로젝트는 다양한 웹 보안 취약점을 실습할 수 있는 교육용 애플리케이션입니다. CSRF 관련 실습도 포함되어 있으니 참고해보세요. WebGoat GitHub

  2. Postman을 활용한 테스트: Postman과 같은 API 테스트 도구를 사용하여 CSRF 토큰을 포함하지 않은 요청을 시도해보고, Spring Security가 이를 어떻게 처리하는지 확인해보세요.

  3. SameSite 쿠키 설정 실습: SameSite 쿠키 속성을 설정하여 CSRF 공격을 추가로 방어하는 방법을 실습해보세요. 이는 CSRF 보호를 강화하는 좋은 방법입니다.

  4. 실제 프로젝트에 적용: 개인 프로젝트나 학습 프로젝트에 CSRF 보호를 적용하고, 다양한 공격 시나리오를 시도해보며 이해를 깊이 있게 해보세요.


결론

이번 실습을 통해 CSRF의 개념을 이해하고, Spring Security를 사용하여 이를 효과적으로 방어하는 방법을 배웠습니다. 실제 프로젝트에서 이러한 보안 메커니즘을 올바르게 구현하는 것은 매우 중요하며, 이는 신입 개발자로서의 역량을 크게 향상시킬 것입니다. 추가적인 보안 주제도 학습하여 더욱 안전한 웹 애플리케이션을 개발할 수 있도록 노력해보세요!

0개의 댓글