전체 그래프
OWASP Top 10

CSRF

web-securityowaspcsrfattack

상위: Web Security

요약

CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)는 사용자가 의도하지 않은 요청을 실행하게 만드는 공격입니다. 피해자의 인증된 세션을 이용해 비밀번호 변경, 송금 등의 악성 행위를 수행합니다.

공격 원리

1. 피해자가 은행 사이트에 로그인 (세션 쿠키 발급)
2. 피해자가 공격자의 악성 사이트 방문
3. 악성 사이트에서 은행으로 송금 요청 전송
4. 브라우저가 자동으로 세션 쿠키 포함하여 요청
5. 은행은 정상 요청으로 인식하여 처리
공격자 악성 사이트              피해자 브라우저              은행 서버
                                                             
         악성 페이지 로드                                 
                                                             
                                     ─── 송금 요청 ────────▶ 
                                       (세션 쿠키 자동 포함)   
                                                             
                                     ◀─── 송금 성공 ─────── 

공격 시나리오

시나리오 1: 비밀번호 변경

정상적인 비밀번호 변경 폼:

<form action="https://bank.com/change-password" method="POST">
  <input name="new_password" value="새비밀번호">
  <button>변경</button>
</form>

공격자의 악성 페이지:

<!-- 피해자가  페이지를 열면 자동으로 비밀번호 변경 -->
<body onload="document.forms[0].submit()">
  <form action="https://bank.com/change-password" method="POST">
    <input name="new_password" value="hacker123">
  </form>
</body>

시나리오 2: 송금

<!-- 이미지 태그로 GET 요청 -->
<img src="https://bank.com/transfer?to=attacker&amount=1000000" style="display:none">

<!-- 또는 숨겨진 폼으로 POST 요청 -->
<iframe name="csrf-frame" style="display:none"></iframe>
<form action="https://bank.com/transfer" method="POST" target="csrf-frame">
  <input name="to" value="attacker">
  <input name="amount" value="1000000">
</form>
<script>document.forms[0].submit();</script>

시나리오 3: 관리자 계정 생성

<form action="https://admin.company.com/create-admin" method="POST">
  <input name="username" value="hacker">
  <input name="password" value="hacker123">
  <input name="role" value="admin">
</form>
<script>document.forms[0].submit();</script>

XSS vs CSRF 비교

구분XSSCSRF
공격 위치피해자 브라우저에서 스크립트 실행피해자의 인증 세션 이용
필요 조건스크립트 삽입 가능한 취약점피해자가 로그인 상태
할 수 있는 일모든 것 (쿠키 탈취, DOM 조작 등)인증된 요청만 가능
응답 확인가능불가능 (Same-Origin Policy)

방어 방법

1. CSRF Token (가장 효과적)

서버에서 토큰 생성:

# Django
from django.middleware.csrf import get_token

def form_view(request):
    csrf_token = get_token(request)
    return render(request, 'form.html', {'csrf_token': csrf_token})

폼에 토큰 포함:

<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="abc123xyz">
  <input name="to" placeholder="받는 사람">
  <input name="amount" placeholder="금액">
  <button>송금</button>
</form>

서버에서 검증:

def transfer(request):
    if request.POST['csrf_token'] != request.session['csrf_token']:
        return HttpResponse("CSRF 공격 탐지", status=403)
    # 정상 처리

원리:

공격자는 csrf_token 값을   없음 (Same-Origin Policy)
 올바른 토큰 없이는 요청 실패

2. SameSite 쿠키

Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
설명CSRF 방어
Strict외부 사이트에서 쿠키 전송 안 함완벽
LaxGET 요청은 허용, POST는 차단부분적
None항상 전송 (Secure 필수)없음
SameSite=Strict 설정 :
공격자 사이트  bank.com 요청  쿠키 미포함  인증 실패

3. Referer/Origin 헤더 검증

def transfer(request):
    origin = request.headers.get('Origin')
    referer = request.headers.get('Referer')

    allowed_origins = ['https://bank.com', 'https://www.bank.com']

    if origin not in allowed_origins:
        return HttpResponse("CSRF 공격 탐지", status=403)

한계:

  • Referer 헤더가 없을 수 있음 (프라이버시 설정)
  • 우회 가능한 경우 존재

4. Double Submit Cookie

// 쿠키와 헤더에 동일한 값 전송
document.cookie = "csrf_token=abc123";

fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': 'abc123'  // 헤더로도 전송
    },
    body: JSON.stringify({to: 'friend', amount: 10000})
});

원리:

  • 공격자는 Same-Origin Policy로 인해 쿠키 값을 읽을 수 없음
  • 헤더에 올바른 값을 설정할 수 없음

5. 중요 작업에 재인증 요구

def transfer(request):
    # 송금  비밀번호 재확인
    if not verify_password(request.POST['password']):
        return HttpResponse("비밀번호 확인 필요", status=403)

프레임워크별 CSRF 보호

Django

# settings.py
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',  # 기본 활성화
]

# 템플릿
<form method="POST">
    {% csrf_token %}
    ...
</form>

Spring

// 기본 활성화 (Spring Security)
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        http.csrf(); // 기본 활성화
        return http.build();
    }
}

Express.js

const csrf = require('csurf');
app.use(csrf({ cookie: true }));

app.get('/form', (req, res) => {
    res.render('form', { csrfToken: req.csrfToken() });
});

테스트 방법

1. 토큰 제거 테스트

1. 정상 요청에서 CSRF 토큰 제거
2. 요청이 성공하면 취약

2. 토큰 재사용 테스트

1. 유효한 토큰을 다른 세션에서 사용
2. 요청이 성공하면 취약

3. 메서드 변경 테스트

POST  GET으로 변경하여 테스트
GET 요청에는 CSRF 보호가 없을  있음

CSRF 공격이 불가능한 경우

  1. API 토큰 인증: Bearer Token은 자동 전송되지 않음
  2. Custom Header 필수: X-Requested-With 등 커스텀 헤더 필요 시
  3. JSON Content-Type: 일반 폼으로 JSON 전송 불가

관련 공격

참고 자료