[Spring] What - CSRF란?

하쮸·2025년 10월 18일

Error, Why, What, How

목록 보기
45/62

1. CSRF란?

  • CSRF(Cross Site Request Forgery), 한국어로 직역하면 교차 사이트 요청 위조라 할 수 있다.
    • 사용자가 로그인된 상태에서 의도하지 않은 요청을 보내도록 유도하는 웹 보안 공격임.
    • 공격자는 악성 웹사이트에 사용자가 로그인된 웹사이트로 요청을 보내도록 하는 악성 코드를 심어놓고, 사용자가 해당 악성 웹사이트를 방문하는 순간 사용자의 개인정보나 자산을 탈취하는 등 악의적인 작업을 수행하게 함.

1-1. Ex.

  • 은행 웹사이트에서 현재 로그인된 사용자가 다른 은행 계좌로 돈을 이체할 수 있는 폼을 제공한다고 가정함.
    • 예를 들어 이체 폼은 아래(↓)와 같음.
<form method="post" action="/transfer">
    <input type="text" name="amount"/>
    <input type="text" name="routingNumber"/>
    <input type="text" name="account"/>
    <input type="submit" value="Transfer"/>
</form>
  • ↑ 이 폼이 전송될 때, 아래(↓)와 같은 HTTP 요청이 만들어짐.
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

  • 이제 은행 웹사이트에 로그인한 상태(로그아웃 X)에서 악성 웹 사이트(evil website)를 방문했다고 가정함.
    • 이때 악성 웹 사이트는 아래와 같은 HTML 페이지를 포함하고 있음.
<form method="post" action="https://bank.example.com/transfer">
    <input type="hidden" name="amount" value="100.00"/>
    <input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
    <input type="hidden" name="account" value="evilsAccountNumber"/>
    <input type="submit" value="Win Money!"/>
</form>
  • 이러한 상황 속에서 사용자가 “Win Money!” 버튼을 클릭하는 순간
    사용자의 은행 계좌에서 100달러가 악성 사용자의 계좌로 이체되어버림.

    • 이 일이 가능한 이유는 악성 웹 사이트는 사용자의 쿠키 값을 볼 수는 없지만, 브라우저는 요청 시 은행의 쿠키(JSESSIONID 등)를 자동으로 포함하기 때문임.
  • 더 나쁜 점은 이 과정이 자바스크립트를 통해 자동화될 수도 있다는 것.

    • 즉, 사용자가 클릭조차 하지 않아도 이체가 이루어질 수 있음.
    • 또한 신뢰할 수 있는 사이트가 XSS(=Cross-Site Scripting, 교차 사이트 스크립팅) 공격을 당했을 때도 같은 방식으로 악용될 수 있음.
  • 그렇다면 이러한 공격으로부터 사용자를 어떻게 보호할 수 있을까?


2. CSRF 공격으로부터 방어하기.

  • CSRF 공격이 가능한 이유는 피해자 웹사이트에서 보낸 요청과 공격자 웹사이트에서 보낸 요청이 완전히 동일하게 보이기 때문임.
    • 따라서 서버는 해당 요청이 정상적인 사이트에서 온 것인지? 악성 사이트에서 온 것인지? 구분할 방법이 없음.
  • 이를 방지하려면 요청 안에 악성 사이트에서는 절대 보낼 수 없는 고유한 정보(토큰)가 포함되어 있어야함.
    • Spring의 경우 이를 위해 두 가지 방어 메커니즘을 제공함.
      • Synchronizer Token Pattern (동기화 토큰 패턴)
      • 세션 쿠키에 SameSite 속성 지정.
    • 두 방식 모두 '안전한(Safe)' HTTP 메서드가 반드시 읽기 전용(Read-only) 이어야 한다는 전제 조건이 있음.

2-1. 안전한 HTTP 메서드는 읽기 전용이어야 함.

  • CSRF 방어를 적용하려면, 애플리케이션이 안전한 HTTP 메서드(GET, HEAD, OPTIONS, TRACE)를
    항상 읽기 전용 요청으로 처리해야 함.
    • 즉, 이 메서드들로는 애플리케이션의 상태를 변경해서는 안됨.

2-2. Synchronizer Token Pattern (동기화 토큰 패턴)

  • CSRF 공격으로부터 보호하는 가장 보편적이고 포괄적인 방법은 동기화 토큰 패턴(Synchronizer Token Pattern)을 사용하는 것.

    • 이 방식은 각 HTTP 요청에 세션 쿠키 외에도 CSRF 토큰(CSRF token)이라고 불리는 난수 생성 값이 HTTP 요청에 포함되도록 함.
  • 요청이 서버로 전송되면 서버는 요구하는 CSRF 토큰(expected token)과 요청에 포함된 실제 CSRF 토큰(actual token)을 비교하고 두 개가 일치하지 않으면 요청을 거부함.

    • 핵심은 CSRF 토큰이 브라우저가 자동으로 추가하지 않는 HTTP 요청의 일부에 포함되어야함.
    • 즉, CSRF 토큰은 HTTP 파라미터 또는 HTTP 헤더에 있어야 하며 쿠키에는 포함되어서는 안됨.
      (쿠키는 브라우저가 자동으로 전송하기 때문.)

2-2-1. Ex.

  • CSRF 토큰이 포함된 송금 요청 폼 예시.

  • CSRF 토큰을 _csrf라는 파라미터로 요구한다고 가정하면.

<form method="post" action="/transfer">
    <input type="hidden" name="_csrf" value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
    <input type="text" name="amount"/>
    <input type="text" name="routingNumber"/>
    <input type="text" name="account"/>
    <input type="submit" value="Transfer"/>
</form>
  • 외부 사이트는 동일 출처 정책(Same-Origin Policy)에 의해 응답 내용을 읽을 수 없으므로 CSRF 토큰 값을 알 수 없음.

  • 요청은 아래와 같이 전송됨.

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
  • 이제 HTTP 요청에는 _csrf 파라미터가 포함되어 있고 공격자는 이 토큰 값을 맞출 수 없기 때문에 요청 검증에 실패함.

2-3. SameSite 속성.

  • CSRF 공격을 방어하는 또 다른 방법은 쿠키에 SameSite 속성을 지정하는 것.

    • 서버는 쿠키를 설정할 때 SameSite 속성을 지정하여 외부 사이트에서 요청이 올 때는 쿠키가 전송되지 않도록 지시할 수 있음.
  • Spring Security는 세션 쿠키를 생성하지 않으므로 SameSite 속성을 직접 제어하지 않음.

    • Spring Session은 서블릿 기반 애플리케이션에서 SameSite 속성을 지원함.
    • Spring Framework의 CookieWebSessionIdResolver는 WebFlux 기반 애플리케이션에서 SameSite 속성을 즉시 지원함.

2-3-1. Ex.

  • SameSite 속성이 포함된 HTTP 응답 헤더의 예.
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
  • SameSite 속성의 유효한 값은 아래와 같음.

    • Strict
      • 이 옵션을 지정하면 동일 사이트에서 들어오는 모든 요청에 쿠키가 포함됨.
        그렇지 않은 경우에는 쿠키가 HTTP 요청에 포함되지 않음.
    • Lax
      • 이 옵션을 지정하면 같은 사이트에서 요청이 들어 오거나 최상위 탐색에서 요청이 들어오고 메서드가 읽기 전용일 때 쿠키가 전송됨.
        그렇지 않은 경우에는 쿠키가 HTTP 요청에 포함되지 않음.
  • 세션 쿠키에 SameSite 속성이 설정되면 브라우저는 정상적인 웹사이트에서 오는 요청에는 계속해서 JSESSIONID 쿠키를 전송함.

    • 그러나 악성 웹사이트에서 오는 요청에는 더 이상 브라우저가 JSESSIONID 쿠키를 전송하지 않음.
  • CSRF 공격 방어를 위해 SameSite 속성을 사용할 때 주의해야 할 몇 가지 중요한 고려 사항이 있습니다.


  • SameSite 옵션을 Strict로 설정하면 더 강력한 방어가 제공되지만 사용자를 혼란스럽게 할 수도 있음.

    • social.example.com에 호스팅된 소셜 미디어 사이트에 로그인 상태를 유지하는 사용자가 있을 경우.
      • 사용자는 email.example.org에서 소셜 미디어 사이트로의 링크가 포함된 이메일을 받고 사용자가 이 링크를 클릭하면 소셜 미디어 사이트에 인증될 것으로 당연히 예상하지만 SameSite 속성이 Strict인 경우에는 쿠키가 전송되지 않으므로 사용자는 인증되지 않음.
  • 또 다른 고려 사항은 SameSite 속성이 사용자를 보호하기 위해서는 브라우저가 SameSite 속성을 지원해야 함.

    • 대부분의 최신 브라우저는 SameSite 속성을 지원하지만 여전히 사용되는 일부 구형 브라우저는 지원하지 않을 수 있음.
  • 위와 같은 이유로 일반적으로 우리는 SameSite 속성을 CSRF 공격에 대한 유일한 보호 수단이 아닌 심층 방어(defense in depth)의 한 부분으로 사용할 것을 권장함.


3. CSRF 방어는 언제 사용해야 하는가?

  • 일반 사용자가 브라우저에서 처리할 수 있는 모든 요청에 대해 CSRF 방어를 사용하는 것이 좋음.
    • 브라우저가 아닌 클라이언트(non-browser clients)만 사용하는 서비스를 생성하는 경우에는 CSRF 보호를 비활성화하는 것이 좋음.

3-1. CSRF 방어와 JSON.

  • "JavaScript가 만드는 JSON 요청도 보호해야 하는가?"
    • 이에 대한 답은 "상황에 따라 다르다"임.
    • 하지만 JSON 요청에 영향을 미칠 수 있는 CSRF 악용 사례가 있으므로 매우 신중해야 함.

3-1-1. Ex.

  • 악성 사용자는 아래와 같은 폼을 사용하여 JSON으로 CSRF를 생성할 수 있음.
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
	<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
	<input type="submit"
		value="Win Money!"/>
</form>
  • 위 코드는 아래와 같은 JSON 구조를 생성함.
{ "amount": 100,
  "routingNumber": "evilsRoutingNumber",
  "account": "evilsAccountNumber",
  "ignore_me": "=test"
}
  • 만약 애플리케이션이 Content-Type 헤더를 검증하지 않는다면 이 악용에 노출될 수 있음.
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
	<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
	<input type="submit"
		value="Win Money!"/>
</form>
  • 또한 설정에 따라 Content-Type을 검증하는 Spring MVC 애플리케이션이라도 URL 접미사를 위와 같이 .json으로 업데이트하면 여전히 악용될 수 있음.

3-2. CSRF와 Stateless 브라우저 애플리케이션.

  • 애플리케이션이 상태 비저장(stateless)이라면?

    • 그렇다고 반드시 보호된다는 의미는 아님.
    • 사용자가 특정 요청을 위해 웹 브라우저에서 어떤 행동도 수행할 필요가 없다면 그들은 여전히 CSRF 공격에 취약할 가능성이 높음.
  • Ex)

    • 인증을 위해 (JSESSIONID 대신) 모든 상태를 포함하는 사용자 정의 쿠키를 사용하는 애플리케이션일 경우.
      • CSRF 공격이 발생하면 이전 예시에서 JSESSIONID 쿠키가 전송된 것과 똑같은 방식으로 사용자 정의 쿠키가 요청과 함께 전송되므로 이 애플리케이션은 CSRF 공격에 취약함.
  • 기본 인증(Basic Authentication)을 사용하는 애플리케이션 또한 CSRF 공격에 취약함.

    • 브라우저가 이전 예시에서 JSESSIONID 쿠키가 전송된 것과 똑같은 방식으로 사용자 이름과 암호를 모든 요청에 자동으로 포함시키기 때문에 취약함.

4. CSRF 고려 사항.

  • CSRF 공격에 대한 방어를 구현할 때 고려해야 할 몇 가지 특별한 사항이 있음.

4-1. 로그인 (Logging In)

  • 로그인 요청을 위조하는 것을 방지하기 위해 로그인 HTTP 요청은 CSRF 공격으로부터 보호되어야 함.

    • 로그인 요청 위조 방지는 악성 사용자가 피해자의 민감한 정보를 읽을 수 없도록 하기 위해 필요함.
  • 공격은 아래와 같이 수행됨.

    • 악의적인 사용자는 악의적인 사용자의 자격 증명으로 CSRF 로그인을 수행함.
      이제 피해자는 악의적인 사용자로 인증됨.
    • 공격자(악의적인 사용자)는 피해자를 속여 손상된(비정상적인) 웹사이트에 접속하게 만들고 피해자가 그곳에 민감한 정보를 입력하도록 유도함.
    • 피해자가 입력한 정보는 공격자의 계정에 연결되므로, 공격자는 자신의 계정으로 로그인하여 피해자의 민감한 정보를 확인할 수 있음.
  • 로그인 HTTP 요청이 CSRF 공격으로부터 보호되도록 하는 과정에서 발생할 수 있는 문제점은 세션 타임아웃(session timeout)이 발생할 수 있다는 점.

    • 세션이 만료되면 요청이 거부될 수 있는데 로그인 과정에서 세션이 필요하다고 예상하지 못한 사용자는 당황스러울 수 있음.

4-2. 로그아웃 (Logging Out)

  • 로그아웃 요청 위조를 방지하기 위해 로그아웃 HTTP 요청은 CSRF 공격으로부터 보호되어야 함.

    • 로그아웃 요청 위조 방지는 악성 사용자가 피해자의 민감한 정보를 읽을 수 없도록 하기 위해 필요.
  • 로그아웃 HTTP 요청을 CSRF 공격으로부터 보호할 때도, 세션이 만료되어 요청이 거부되는 문제가 발생할 수 있습니다.

    • 로그아웃 과정에서 세션이 필요하다고 예상하지 못한 사용자에게는 마찬가지로 당황스러울 수 있음.

4-3. CSRF와 세션 타임아웃(CSRF and Session Timeouts)

  • 일반적으로 서버는 요구하는 CSRF 토큰(expected CSRF token)을 세션에 저장함.

    • 즉, 세션이 만료되는 즉시, 서버는 CSRF 토큰을 찾을 수 없게 되어 HTTP 요청을 거부하게 됨.
  • 이 문제를 해결하기 위한 몇 가지 방법(각각 장단점이 있음)은 아래와 같음.

    • JavaScript를 사용하여 폼 제출 시점에 CSRF 토큰을 새로 요청.
      • 폼이 제출되기 직전에 새 토큰을 받아 폼에 삽입한 후 전송.
      • 세션 만료 문제를 가장 효과적으로 해결할 수 있는 방법임.
  • 세션 만료가 임박했음을 사용자에게 알려주는 JavaScript 구현.

    • 경고 메시지나 팝업을 통해 사용자가 버튼을 클릭해서 세션을 연장하도록 유도함.
  • CSRF 토큰을 쿠키에 저장하는 방법.

    • 단, 이렇게 할 경우 CSRF 토큰의 수명이 세션보다 길어질 수 있음.

  • 왜 기본적으로 CSRF 토큰을 쿠키에 저장하지 않을까?
    • 그 이유는 다른 도메인에서 HTTP 헤더(예: Cookie 설정용 헤더)를 조작하여 설정할 수 있는 취약점이 있기 때문임.
    • 이는 Ruby on Rails가 더 이상 X-Requested-With 헤더가 있을 때 CSRF 검사를 생략하지 않도록 변경한 이유와 같음.
    • 또한 상태(state)를 제거하면(즉, 세션 만료가 없으면) 토큰이 유출되었을 때 강제로 무효화할 수 없다는 단점이 있음.

5. Multipart (파일 업로드)

  • multipart/form-data 형식의 요청(파일 업로드)을 CSRF로부터 보호하는 것은 일종의 닭이 먼저냐, 달걀이 먼저냐 문제를 야기함.

    • CSRF 공격을 방지하려면 요청 본문(body)을 읽어 토큰을 확인해야 하지만 본문을 읽는 시점에 이미 파일이 업로드되기 때문.
    • 즉, 외부 사이트가 파일을 서버로 업로드할 수 있는 상황이 생김.
  • 이를 해결하기 위한 두 가지 방법은 아래와 같고 각각 장단점이 존재함.

    • CSRF 토큰을 요청 본문(Body)에 포함하기. (Place CSRF Token in the Body)
      • 이 경우, 본문은 인가(authorization) 전에 읽히므로, 누구나 서버에 임시 파일을 업로드할 수는 있지만, 실제 처리가 이루어지는 것은 인증된 사용자만 가능.
      • 일반적으로 이 방법이 권장되며 대부분의 서버에서 임시 파일 업로드는 큰 문제가 되지 않음.
    • CSRF 토큰을 URL(Query Parameter)에 포함하기. (Include CSRF Token in URL)
      • 인증되지 않은 사용자가 임시 파일을 업로드하는 것이 허용되지 않는 경우, 대안으로 form의 action 속성에 쿼리 파라미터 형태로 CSRF 토큰을 포함할 수 있음.
      • 그러나 이 접근 방식의 단점은 쿼리 파라미터가 쉽게 노출될 수 있다는 점임.
        • 일반적으로는 민감한 데이터를 본문(body)이나 헤더(header)에 포함시켜 유출을 방지하는 것이 모범 사례.
  • Spring Security의 CSRF 보호 기능을 멀티파트(Multipart) 파일 업로드와 통합하기 전에, 먼저 CSRF 보호 없이도 업로드가 가능한지 확인해야 함.


5-1. HiddenHttpMethodFilter

  • 일부 애플리케이션은 폼 파라미터를 사용하여 HTTP 메서드를 재정의(override) 할 수 있음.
    • Ex) 아래와 같은 폼은 실제로는 POST 요청이지만, _method 값을 통해 DELETE 요청처럼 처리할 수 있음.
<form action="/process" method="post">
	<!-- ... -->
	<input type="hidden" name="_method" value="delete"/>
</form>
  • 이러한 HTTP 메서드 재정의는 필터(Filter) 에서 수행되며 반드시 Spring Security의 필터보다 먼저 배치되어야 함.

  • 여기서 재정의가 POST 요청에서만 발생하므로 실제로 큰 문제가 되는 경우는 드물지만, 보안상의 모범 사례로는 항상 Spring Security 필터보다 앞에 위치시키는 것이 바람직함.


6. 참고.

profile
Every cloud has a silver lining.

0개의 댓글