CSRF (Cross-Site Request Forgery)

개요

정의: 공격자가 사용자 모르게 로그인된 웹사이트에 위조된 요청을 보내도록 유도하는 공격

핵심 원인 — 브라우저의 쿠키 자동 전송

  • CSRF가 가능한 근본 이유는 브라우저가 요청을 보낼 때 해당 도메인의 쿠키를 자동으로 첨부하기 때문
  • 악성 사이트에서 example.com으로 요청을 보내도, 브라우저는 example.com의 쿠키를 자동으로 붙여서 보냄
사용자가 example.com에 로그인 → 브라우저에 세션 쿠키 저장

악성 사이트 방문

악성 사이트가 example.com으로 요청 전송

브라우저가 example.com 쿠키를 자동 첨부

서버는 정상 요청으로 판단

작동 방식:

  1. 사용자가 example.com에 로그인한 상태에서 공격자가 만든 악성 사이트 방문
  2. 악성 사이트가 사용자 브라우저를 통해 example.com에 위조된 요청 전송
  3. 브라우저가 쿠키를 자동 첨부하므로 서버는 정상 요청으로 처리

XSS와의 차이

| | CSRF | XSS | |-----------|-----------------------------|-----------------------------| | 공격 목적 | 사용자 권한으로 요청 위조 | 악성 스크립트 실행 | | 공격 경로 | 다른 사이트에서 요청 유도 | 같은 사이트에 스크립트 삽입 | | 피해 | 의도치 않은 상태 변경 | 정보 탈취, 세션 하이재킹 | | 인증 필요 | 사용자가 로그인 상태여야 함 | 무관 |

예시

시나리오: 사용자가 example.com에 로그인한 상태에서 CSRF 공격을 받는 경우

1. 공격

<!-- 공격자의 악성 사이트 -->
<form action="https://example.com/change-password" method="POST">
  <input type="hidden" name="password" value="hacked1234">
</form>
<script>
  document.forms[0].submit(); // 페이지 로드 즉시 자동 제출
</script>

사용자가 이 페이지를 방문하는 순간, 본인도 모르게 비밀번호 변경 요청이 전송됩니다.

2. 영향

  • 서버는 쿠키가 첨부된 정상 요청으로 판단
  • 사용자 의사와 무관하게 비밀번호 변경, 계정 정보 수정, 송금 등이 발생

3. 대비 방법

CSRF 토큰

  • 서버가 예측 불가능한 토큰을 발급하고, 요청마다 이를 검증하는 방식
  • 악성 사이트는 이 토큰을 알 수 없기 때문에 위조 요청이 차단됨
서버가 토큰 발급 → 클라이언트가 요청에 포함 → 서버가 검증
악성 사이트는 토큰을 모름 → 검증 실패 → 요청 거부
// 클라이언트
function ChangePasswordForm() {
  const [csrfToken, setCsrfToken] = useState('');
  const [password, setPassword] = useState('');

  useEffect(() => {
    // 서버에서 토큰 발급받기
    fetch('/csrf-token')
      .then(res => res.json())
      .then(data => setCsrfToken(data.csrfToken));
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    await fetch('/change-password', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken  // 헤더에 토큰 포함
      },
      body: JSON.stringify({ password })
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="hidden" name="_csrf" value={csrfToken} />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
      />
      <button type="submit">Change Password</button>
    </form>
  );
}
// Express 서버 — csrf-csrf 사용
const express = require('express');
const { doubleCsrf } = require('csrf-csrf');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser());
app.use(express.json());

const { generateToken, doubleCsrfProtection } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET,
  cookieName: '__Host-psifi.x-csrf-token',
});

// 토큰 발급
app.get('/csrf-token', (req, res) => {
  res.json({ csrfToken: generateToken(req, res) });
});

// 보호가 필요한 라우트에 미들웨어 적용
app.post('/change-password', doubleCsrfProtection, (req, res) => {
  // 토큰 검증 통과 후 실행
  res.send('Password changed');
});

SameSite 쿠키 설정

  • 쿠키가 동일한 사이트에서의 요청에만 전송되도록 제한
  • 브라우저 레벨에서 CSRF를 원천 차단하는 가장 간단한 방법
res.cookie('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict'  // 타 사이트에서의 요청엔 쿠키 전송 안함
});
SameSite 값동작
Strict타 사이트 요청 시 쿠키 전송 완전 차단
Lax타 사이트에서의 GET 요청은 허용, POST 등은 차단 (기본값)
None모든 요청에 쿠키 전송 (Secure 필수)

Origin Header 검증

  • 서버에서 요청의 Origin 헤더를 확인해 신뢰할 수 있는 출처인지 검증
  • 서버 레벨에서 검증하는 것이기 때문에 CORS 헤더 추가와는 다른 개념
const allowedOrigins = ['https://example.com'];

app.use((req, res, next) => {
  const origin = req.get('origin');

  // POST, PUT, DELETE 등 상태 변경 요청에 대해서만 검증
  if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
    if (!origin || !allowedOrigins.includes(origin)) {
      return res.status(403).json({ error: 'CSRF detected: origin not allowed' });
    }
  }

  next();
});

Origin 헤더는 드물게 없는 경우도 있어서 단독 사용보다는 CSRF 토큰 또는 SameSite와 함께 쓰는 게 안전


예방 전략 요약

방법방어 원리단독 사용
CSRF 토큰서버만 아는 토큰으로 요청 검증가능
SameSite 쿠키타 사이트 요청 시 쿠키 전송 차단Strict면 가능
Origin Header 검증요청 출처를 서버에서 직접 확인단독 사용 비권장

현대 브라우저는 SameSite=Lax가 기본값이라 과거보다 CSRF 위험이 낮아졌지만, 민감한 작업(비밀번호 변경, 결제 등)에는 CSRF 토큰을 함께 사용하는 것이 권장됨

Backlinks 3