← 목록으로

CSRF (Cross-Site Request Forgery): 원리와 방어

[Web] Web · 작성: 2026-07-19 19:15:17 · 수정: 2026-07-21 22:53:06 · 조회 21

CSRF는 "공격자가 내 브라우저 대신 요청을 보내는 취약점"이라고 설명하면 절반만 맞다. 진짜 질문은 **"브라우저는 왜 내가 요청하지도 않은 걸 내 자격(쿠키)으로 자동 실행해버리는가"**다. 이 글은 이 블로그의 실제 CSRF 방어 코드(middleware/csrf.js)를 놓고, 브라우저 네트워크 스택의 쿠키 자동 첨부 메커니즘부터 방어를 무력화하는 체이닝까지 따라간다.

소스 레벨: 이 블로그는 CSRF를 어떻게 막고 있나

middleware/csrf.js는 세션에 저장된 랜덤 토큰과, 폼이 제출한 _csrf 값을 비교하는 **동기화 토큰 패턴(Synchronizer Token Pattern)**이다.

if (!req.session.csrfToken) {
  req.session.csrfToken = crypto.randomBytes(24).toString('hex');
}
res.locals.csrfToken = req.session.csrfToken;
const token = req.body && req.body._csrf;
if (!token || token !== req.session.csrfToken) {
  // 거부
}

이 방어가 성립하는 이유는 단 하나 — req.session.csrfToken은 서버가 세션 스토어에 보관하고, 페이지를 렌더링할 때만 폼 안에 hidden input으로 심어주기 때문에, 공격자 사이트는 이 값을 알 방법이 없다. 공격자가 아무리 피해자 브라우저를 통해 요청을 강제로 쏘게 만들어도, _csrf 필드에 맞는 값을 채우지 못하면 서버가 거부한다.

커널/브라우저 런타임 레벨: 왜 애초에 이런 방어가 필요한가

CSRF가 성립하는 근본 원인은 서버 로직이 아니라 브라우저 네트워크 스택의 쿠키 자동 첨부 알고리즘에 있다. 브라우저 내부의 쿠키 저장소(Cookie Jar)는 요청을 보낼 때마다 아래 알고리즘(RFC 6265)을 그대로 수행한다.

  1. 요청 대상 URL의 domain, path를 확인
  2. 쿠키 저장소를 순회하며 domain/path가 일치하는 모든 쿠키를 찾음 — 이 매칭 로직은 "이 요청을 누가(어떤 오리진의 페이지가) 시작했는가"를 전혀 고려하지 않는다
  3. SameSite 속성이 있으면 요청이 **동일 사이트에서 시작됐는지(top-level navigation, same-site iframe 등)**를 추가로 판단해 첨부 여부 결정
  4. 조건을 만족하는 쿠키를 Cookie: 헤더에 실어 요청과 함께 전송

3번의 SameSite 검사가 추가되기 전까지(2016년 이전 기본 동작), 브라우저는 도메인/경로만 맞으면 요청의 출처가 어디든 쿠키를 무조건 실어 보냈다. 이게 CSRF의 정확한 근본 원인이다 — evil.com에 있는 <form>bank.com으로 요청을 보내도, 브라우저 관점에서는 그냥 "bank.com에 대한 요청이니 bank.com 쿠키를 첨부"할 뿐이다. 브라우저는 **요청의 목적지(도메인)**만 보지 **요청의 출발지(어떤 페이지가 이 요청을 만들었는가)**는 쿠키 첨부 판단에 근본적으로 관여시키지 않는다는 게 이 취약점 클래스의 뿌리다.

SameSite=Lax(현재 대부분 브라우저의 기본값)가 이걸 상당 부분 완화하지만 완전하진 않다 — GET 방식의 top-level navigation은 Lax에서도 쿠키가 첨부된다. 상태를 변경하는 작업을 GET으로 구현해둔 레거시 엔드포인트(GET /account/delete?id=1 같은)가 있다면 SameSite=Lax만으로는 못 막는다는 뜻이다. 이 블로그의 POST /blog/:slug/delete처럼 상태 변경을 반드시 POST로만 받게 설계한 것도 이 완화를 보강하는 조치다.

페이로드가 서버에서 실제로 작동하는 순서

  1. 공격자가 evil.com에 아래와 같은 자동 제출 폼을 심어둠
<form action="https://martina7.com/blog/some-post/delete" method="POST" id="f">
</form>
<script>document.getElementById('f').submit();</script>
  1. 로그인 상태인 피해자가 evil.com을 방문 → 브라우저가 위 스크립트를 실행하며 martina7.com으로 POST 요청을 자동 전송
  2. 브라우저 네트워크 스택이 요청 목적지가 martina7.com이라는 이유만으로 피해자의 세션 쿠키를 Cookie: 헤더에 자동 첨부
  3. 서버는 유효한 세션 쿠키를 받았으니 req.session을 정상 복원 — 인증(Authentication) 관점에서는 완전히 정상 요청
  4. 다만 이 블로그는 _csrf 필드가 폼에 없으므로 verifyCsrfToken 미들웨어에서 !token 조건에 걸려 즉시 거부 — 인증은 통과했지만 "이 요청이 진짜 사용자 의도로 만들어졌는가"를 검증하는 별도 계층에서 막힌 것이 핵심이다. 이 미들웨어가 없었다면 4번에서 바로 삭제가 실행됐을 것이다.

여기서 끝나지 않는다: CSRF 체이닝

1) XSS와 결합 — CSRF 토큰 자체를 훔쳐서 우회 (가장 강력한 체인)

동기화 토큰 패턴의 유일한 전제는 "공격자가 토큰 값을 모른다"는 것이다. 만약 대상 사이트에 XSS가 하나라도 있다면, 공격자 페이지가 아니라 취약한 사이트 자신의 페이지에서 실행되는 스크립트가 되므로 Same-Origin Policy에 걸리지 않고 DOM에서 토큰을 직접 읽어올 수 있다.

fetch('/blog/new').then(r => r.text()).then(html => {
  const token = html.match(/name="_csrf" value="([^"]+)"/)[1];
  fetch('/blog/victim-post/delete', { method: 'POST', headers: {'Content-Type':'application/x-www-form-urlencoded'}, body: `_csrf=${token}` });
});

CSRF 방어(토큰)와 XSS 방어(이스케이프)는 서로 다른 취약점 클래스를 막는 것 같지만, 실제로는 XSS 하나가 CSRF 방어의 유일한 전제조건을 정확히 무너뜨린다. 이게 XSS 글에서 다룬 체이닝과 같은 지점에서 만난다 — 보안 점검에서 "XSS가 있으면 CSRF는 이미 뚫린 것으로 간주"하는 이유가 여기 있다.

2) 로그인 CSRF → 세션 고정(Session Fixation)

공격자가 자기 계정으로 로그인시키는 CSRF를 피해자 브라우저에서 실행시키면, 피해자가 모르는 사이 공격자 계정으로 로그인된 상태가 된다. 이 상태에서 피해자가 민감한 정보(결제 정보, 개인정보)를 입력하면 그게 공격자 계정에 저장되는 시나리오 — 일반적인 "권한 탈취"가 아니라 거꾸로 피해자를 공격자의 계정에 가둬서 데이터를 흘리게 만드는 방향의 체인이라 자주 간과된다.

3) Open Redirect와 결합한 피싱 체인

CSRF 자체는 상태 변경에 특화되어 있지만, 여기에 /redirect?url=처럼 검증되지 않은 리다이렉트 엔드포인트가 있으면 CSRF로 만든 악성 폼의 action을 신뢰할 수 있는 도메인처럼 보이게 위장해 피해자의 의심을 낮추는 데 활용된다.

4) CSRF → SSRF 트리거

서버가 사용자 요청을 받아 대신 외부 URL을 요청하는 기능(SSRF 글 참고)이 있다면, CSRF로 그 요청 자체를 피해자 세션 권한으로 강제 실행시켜서 피해자만 접근 가능한 내부 리소스에 대한 SSRF를 공격자가 원격에서 트리거하는 조합도 가능하다 — CSRF가 "실행 방아쇠", SSRF가 "실제 피해"를 담당하는 체인이다.

탐지와 방어 (요약)

CSRF를 "브라우저가 대신 요청을 보내는 문제"가 아니라 **"브라우저의 쿠키 자동 첨부 알고리즘이 요청의 출처를 판단 기준에 넣지 않는다는 구조적 특성"**으로 이해하면, 왜 SameSite 하나로는 부족하고 토큰 검증이 여전히 필요한지, 그리고 왜 XSS 한 건이 CSRF 방어 전체를 무너뜨리는 체인의 시작점이 되는지가 같은 논리로 설명된다.

Web 카테고리의 글 (3/12)

  1. SQL Injection: 원리, 탐지, 방어
  2. XSS (Cross-Site Scripting): Stored / Reflected / DOM 차이와 방어
  3. CSRF (Cross-Site Request Forgery): 원리와 방어
  4. IDOR / BOLA: 권한 검증이 빠졌을 때
  5. SSRF (Server-Side Request Forgery): 서버가 대신 요청하게 만들기
  6. 파일 업로드 취약점: 웹쉘로 이어지는 경로
  7. 안전하지 않은 역직렬화 (Insecure Deserialization)
  8. SSTI (Server-Side Template Injection): 원리와 탐지
  9. OS Command Injection: 원리와 필터 우회
  10. JWT 취약점: alg=none부터 알고리즘 컨퓨전까지
  11. Race Condition (TOCTOU) 취약점
  12. Path Traversal / LFI (Local File Inclusion): 커널 경로 탐색부터 RCE 체이닝까지
← XSS (Cross-Site Scripting): Stored / Reflected / DOM 차이와 방어 IDOR / BOLA: 권한 검증이 빠졌을 때 →