CSRF (Cross-Site Request Forgery)

#security#CSRF#web
• • •

개요

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

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

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

악성 사이트 방문

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

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

서버는 정상 요청으로 판단

작동 방식:

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

XSS와의 차이

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

예시

시나리오: 사용자가 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 토큰을 함께 사용하는 것이 권장됨

published about 1 year ago · last updated about 2 months ago