지식 공유 | CSRF(Cross‐Site Request Forgery)의 개념과, 방지 방법에 대하여 - DDD-Community/DDD-12-MOYORAK-API GitHub Wiki

image

CSRF가 무엇인지 어렴풋이만 알고 있었지만,
왜 JWT 기반이면 필요 없는지에 대해서는 이야기하지 못하는 모습에 나는 아직 CSRF가 무엇인지 잘 모르는구나 싶었습니다.
이를 기회로 CSRF 무엇인지 확실히 알고자 정리를 하게 되었습니다.

CSRF란 무엇인가

CSRF란, Cross-Site Request Forgery의 줄임말로, 사이트 간 요청 위조를 뜻합니다.
사용자가 로그인한 인증 정보를 악용하여, 사용자가 의도하지 않은 요청을 자동으로 수행하게 만드는 공격을 의미합니다.

상황 예시

  1. https://bank.comuser1 계정으로 로그인에 성공합니다.
  2. 로그인 정보가 쿠키에 저장됩니다.
  3. 그 상태에서 https://dummy.com 사이트에 접속합니다.
  4. 해당 사이트 내부에 아래와 같은 코드가 숨어 있습니다.
<form id="csrfForm" method="POST" action="https://bank.com/transfer">
  <input type="hidden" name="to" value="hacker" />
  <input type="hidden" name="amount" value="100000" />
</form>
  1. 페이지가 열리자마자 아래 스크립트가 실행되어, 자동으로 폼을 전송하게 됩니다.
window.addEventListener('load', function () {
  document.getElementById('csrfForm').submit();
});
  1. 브라우저는 자동으로 로그인된 user1의 쿠키를 포함하여 요청을 보냅니다.
  2. http://bank.com은 유효한 회원 정보를 통해 요청이 들어왔다고 판단하고, 요청을 처리하게 됩니다.

위 같은 공격은, 공격자가 직접 공격하는 것이 아닌,
사용자가 의도하지 않게 요청을 보내도록 유도하는 방식입니다.

브라우저 특성상 자동으로 쿠키를 전송하기 때문에,
서버는 이 요청이 악의적인 요청인지 판단할 수 없고, 막기가 어렵습니다.

방지할 수 있는 수단

CSRF Token

서버에서 각 요청을 구분할 수 있는 고유한 토큰을 발급합니다.
해당 토큰은 사용자의 요청마다 함께 전송되며, 서버는 이 토큰이 유효한지 검증하여 CSRF 공격을 막을 수 있습니다.

방식

  1. 사용자가 특정 페이지에 접속합니다.
  2. 서버는 해당 요청에 대한 CSRF 토큰을 발급하고, 이를 HTML에 숨겨 전달합니다.
<input type="hidden" name="_csrf" value="토큰값" />
  1. 사용자가 Form 제출을 하게 되면, 이때 요청에 CSRF 토큰이 함께 전송됩니다.
  2. 서버는 요청을 처리할 때,
    • 로그인 세션이 유효한지 판단하고,
    • 전송된 CSRF 토큰이 발급한 것과 일치하는지 검증합니다.
  3. 유효한 요청의 경우 이후 처리를 하고, 아니라면 요청 거부합니다.

장점

  • 폼 기반 애플리케이션에 적합합니다.
  • 브라우저 보안 정책(쿠키 SameSite 등)에 의존하지 않고, 독립적으로 동작합니다.
  • 로그인 쿠키가 존재하더라도, 정확한 토큰까지 일치해야하는 이중 장치로 보안성이 높습니다.

단점

  • 클라이언트는 매 요청마다 CSRF 토큰을 포함해야 하므로 구현 및 테스트가 번거롭습니다.
  • 토큰이 노출될 경우, XSS 공격으로 탈취 위험이 존재합니다.

쿠키 SameSite

image

SameSite란, HTTP 쿠키의 속성 중 하나입니다.
브라우저가 쿠키를 요청에 자동으로 포함할지를 결정하는 보안 속성입니다.
즉, 다른 사이트에서 온 요청에 쿠키 정보를 보낼지 말지를 제어합니다.

특히 프론트엔드와 백엔드가 분리된 SPA 구조에서는 이 설정이 중요하게 작용합니다.

속성 종류

None

Set-Cookie: sessionId=abc123; SameSite=None; Secure
  • 어디서 요청하든, 쿠키를 항상 포함하여 요청 보내게 됩니다.
  • 즉, 다른 도메인에서 온 요청에도 쿠키가 전송됩니다.
  • 단, 반드시 Secure 속성과 함께 사용해야 하며, 이는 HTTPS 환경에서만 작동합니다.
    • HTTP 환경에서는 무시됩니다.
  • CSRF 공격에 가방 취약한 속성입니다.

Strict

Set-Cookie: sessionId=abc123; SameSite=Strict
  • 완전 같은 사이트에서의 요청에만 쿠키를 포함합니다.
    • 현재 페이지가 https://zinzo.com이라고 가정한다면,
    • https://zinzo.com 요청에는 쿠키를 포함합니다.
    • https://zinzo-api.com로 요청을 보낸다면, 쿠키는 전송되지 않습니다.
  • 가장 보안성이 높아 CSRF 공격을 방지할 수 있으나, 로그인 유지가 어려울 수 있습니다.

Lax

Set-Cookie: sessionId=abc123; SameSite=Lax
  • GET, HEAD, OPTIONS 등 단순 요청일 때에 쿠키를 전송하게 됩니다.
  • POST, PUT, DELETE 등의 상태 변경 요청에는 쿠키 전송을 하지 않습니다.
    • 단, GET 요청에서도 서버 상태를 바꾸는 행위가 있을 경우에는 안전하지 못하게 됩니다.
  • 보안성과 편의성 사이에 균형 잡힌 설정입니다.

Referer

image

Referer이란, HTTP 헤더 중 하나로
HTTP 요청을 한 현재 페이지의 전체 URL을 담아 서버에 전송하는 헤더입니다.
요청을 받은 서버는 어느 페이지로부터 요청이 온 것인지 파악할 수 있으며,
CSRF를 방어하기 위하여 검증 로직에 활용할 수 있습니다.

방식

  1. 서버에서는 요청이 들어왔을 때, Referer 헤더 값을 확인합니다.
  2. 해당 값이 서버에서 허용하는 주소에서 들어온 것인지를 판단합니다.
  3. 허용되지 않은 출처일 때에는, CSRF 공격으로 간주하고 요청을 거부합니다.
final String referer = request.getHeader("Referer");

if (referer == null || !referer.startsWith("https://zinzo.com")) {
    throw new SecurityException();
}

단점

  • Referer 값은 브라우저의 설정이나 보안 정책 등에 따라 생략될 수 있습니다.
  • Referer 값을 직접 조작이 가능하기 때문에, 무조건 신뢰할 수 없습니다.
  • 전체 URL이 담기기에, 쿼리 파라미터에 민감 정보가 노출될 수 있습니다.
    • 이를 방지하기 위해서 Origin 헤더 사용도 가능합니다.

JWT

JWT(Json Web Token)이란, 사용자가 로그인에 성공하게 되면 서버에서 인증 정보를 토큰으로 만들어 클라이언트에게 전달하는 방식입니다.
클라이언트는 해당 토큰을 매 요청때마다 Authorization 헤더에 포함하여 보내게 됩니다.

이 방식은 CSRF 방어 목적은 아니지만,
브라우저가 Authorization 헤더의 경우 자동으로 전송하지 않기 때문에 CSRF가 성립되지 않아 안전합니다.

인증 흐름

  1. 사용자가 서버에 로그인 요청을 합니다.
  2. 서버는 유효한 회원인지 확인하고, 맞다면 JWT 토큰을 만들어 클라이언트에 응답합니다.
  3. 클라이언트는 요청 때마다, JWT를 Authorization 헤더에 담아 보냅니다.

image

  1. 서버는 헤더의 Authorization가 유효한 토큰인지 판단합니다.
    • 유효하다면, 요청을 정상적으로 처리합니다.
    • 실패의 경우 401 Unauthorized 등의 상태코드로 응답하여 요청을 거부합니다.

장점

  • 해당 토큰의 경우 브라우저가 Authorization 헤더를 자동으로 전송하지 않기 때문에 의도치 않은 요청이 불가능합니다.

단점

  • 클라이언트의 JWT 저장 방식에 따라 보안 취약할 수 있습니다.
    • localStorage -> XSS 취약
    • 쿠키 -> CSRF 취약

JWT의 경우 무상태 인증 방식으로 SPA 구조에서 많이 사용됩니다.
프론트와 서버의 구성이 다르기 때문에 CSRF 공격에 취약할 수 있는 점을 보완할 수 있는 방식입니다.
다만, 프론트에서 토큰 저장 방식에 따른 위험이 존재할 수 있기 때문에 이에 대한 고려가 필요합니다.


마무리

이전 시큐리티 교육 때, CSRF에 대해 분명 듣고 이해했다고 생각했었습니다.
하지만, 이번 코드 리뷰에서 먼저 CSRF 설정에 의문을 갖지 못한 점과
스스로 CSRF가 무엇인지 설명을 하지 못했다는 것을 다시 한번 인지하게 되었습니다.

덕분에 CSRF가 무엇이며, 어떠한 방식으로 방어해야 하는지 정리할 수 있었으며
어떠한 설정이 필요한지도 알게 된 것 같습니다. 😆

⚠️ **GitHub.com Fallback** ⚠️