Studying Web Hacking (CSRF)

CSRF

CSRF란?

CSRF(Cross-Site Request Forgery) : 사이트 간 요청 위조

공격자가 피해자의 브라우저를 속여, 피해자가 로그인된 사이트에 원하지 않는 요청을 보내게 만드는 공격

  • 공격자는 직접 로그인하지 않고 이미 로그인된 사용자를 이용
  • 브라우저가 자동으로 쿠키를 포함해서 요청 보내는 점을 악용

발생하는 이유

브라우저의 기본 동작 때문에 발생

  • 같은 사이트 요청이면 자동으로 쿠키 포함
  • 서버는 쿠키만 보고 “정상 사용자”라고 판단

문제상황

  1. 사용자가 은행 사이트 로그인 상태
  2. 공격자가 악성 페이지 유도
  3. 해당 페이지가 은행 서버로 요청 전송
  4. 브라우저가 자동으로 쿠키 포함
  5. 서버는 정상 요청으로 착각

성립조건

  • 사용자가 대상 사이트에 로그인되어 있어야 함
  • 인증이 주로 쿠키, 세션, 자동 전송 자격증명에 의존해야 함
  • 서버가 CSRF 토큰, Origin 검사, Fetch Metadata 같은 검증을 안하거나 부실해야 함
  • 상태 변경 요청이 존재해야 함

위험이 줄어드는 경우

  • Authorization: Bearer … 같은 헤더 기반 인증만 사용
  • 민감 요청마다 CSRF 토큰 검증
  • SamSite 쿠키 정책 적절히 사용
  • Origin / Referer / Sec-Fetch-Site 검증

공격이 되는 이유

정상 요청

  1. 사용자가 사이트에 로그인
  2. 서버가 세션 쿠키 발급
  3. 사용자가 버튼 클릭
  4. 브라우저가 쿠키 포함 요청 전송
  5. 서버가 처리

CSRF 요청

  1. 사용자는 이미 로그인된 상태
  2. 공격자가 악성 페이지 열게 함
  3. 악성 페이지가 대상 사이트로 요청 생성
  4. 브라우저가 쿠키 자동 첨부
  5. 서버가 “로그인된 사용자의 정상 요청”으로 착각

“사용자가 정말 클릭했는지”가 중요한게 아니라 “쿠키가 있는 요청”이 중요


대표 공격 형태

  1. 고전적인 Form POST 기반 CSRF

    가장 유명한 형태고, 악성 페이지에 숨겨진 form을 두고 자동 제출

    예)

     <!doctype html>
     <html>
     	<body onload="document.forms[0].submit()">
     		<form action="http://localhost:3000/account/email" method="POST">
     			<input type="hidden" name="email" value="attacker@lab.local">
     		</form>
     	</body>
     </html>
    

    이게 먹히려면,

    • 대상 서버가 쿠키 세션 기반 인증
    • CSRF 토큰 검사 없음
    • 쿠키가 cross-site 요청에 포함되는 상황이어야 함
  2. GET 요청 악용

    상태 변경을 GET으로 처리하면 매우 위험

    예)

     <img src="http://localhost:3000/user/delete?id=10">
    

    또는

     <a href="http://localhost:3000/account/logout">클릭</a>
    

    GET 은 원래 조회용

    상태 변경을 GET 으로 두는 것은 최신 기준에서도 여전히 매우 나쁜 설계

  3. 최신 관점: SameSite 때문에 예전보다 달라진 점

    요즘은 브라우저가 예전처럼 아무 쿠키나 다 보내주지 않음

    현대 브라우저 기준 핵심

    • SameSite-=Lax 가 사실상 기본 동작인 경우가 많음
    • SameSite=Strict 는 더 강하게 차단
    • SameSite=None 을 쓰려면 Secure 가 필요
    • SameSite 쿠키 속성 설명

      쿠키의 SameSite 속성은 CSRF 요청 시 쿠키를 전송할지 여부를 제어해서 CSRF 공격을 방어

      • SameSite-=Lax : 기본값

        | 요청 유형 | 쿠키 전송 여부 | | — | — | | 같은 사이트 요청 | 전송 O | | 최상위 탐색 (링크 클릭, GET) | 전송 O | | 크로스 사이트 POST/PUT/PATCH | 차단 X | | <img>, <iframe> 등 서브리소스 | 차단 X |

        • 외부 사이트에서 링크를 클릭해 이동할 때는 쿠키가 전송되지만, 폼 POST나 비동기 요청에서는 차단
        • 대부분의 일반 웹사이트에 적합한 균형잡힌 기본값이다
      • SameSite=Strict : 가장 엄격한 설정

        | 요청 유형 | 쿠키 전송 여부 | | — | — | | 같은 사이트 요청 | 전송 O | | 외부 사이트에서의 모든 요청 | 차단 X |

        • 외부 링크 클릭 후 이동해도 쿠키 미전송 → 로그인이 풀린 것처럼 보일 수 있다
        • 금융, 관리자 페이지 등 보안이 최우선인 서비스에 적합
      • SameSite=None + Secure : 크로스 사이트 요청에서도 쿠키를 항상 전송

        | 요청 유형 | 쿠키 전송 여부 | | — | — | | 같은 사이트 요청 | 전송 O | | 크로스 사이트 모든 요청 | 전송 O |

        • 반드시 Secure 속성과 함께 사용해야 함
          Set-Cookie: token=abc123; SameSite=None; Secure
        
        • Secure 없이 SameSite=None 만 설정하면 브라우저가 쿠키를 거부
        • Secure 속성 자체는 HTTPS 연결에서만 쿠키를 전송하도록 강제
        • 사용 사례: 서드파티 위젯, OAuth/SSO, 결제 모듈, 임베드된 iframe 등
          • 서드파티 위젯: 내 사이트에 다른 회사의 기능을 붙여넣는 것

            예) 내 블로그에 다른 사이트의 위젯(링크)을 걸어놓는 것

            SameSite=None이 없으면 → 쿠키 전송 안됨 → 항상 로그아웃 상태로 보임

          • OAuth / SSO (소셜 로그인) : 네이버 로그인, 구글 로그인 버튼

              예시
                                
              (1) 사용자가 coupang.com에서 "네이버로 로그인" 클릭
              (2) naver.com 로그인 페이지로 이동
              (3) 네이버에서 인증 완료
              (4) coupang.com으로 돌아옴 + "이 사람 인증됐어요" 토큰 전달
              (5) coupang.com이 그 토큰으로 로그인 처리
            

            (4)번 단계에서 네이버 → coupang.com 으로 쿠키/토큰이 넘어오는 순간이 크로스 사이트 요청

            SameSite=Strict면 이 순간 쿠키가 차단돼서 로그인이 실해할 수 있음

          • 결제 모듈 : 내 쇼핑몰에서 토스/네이버페이 결제창을 띄우는 것

            위 로그인과 비슷

          • 임베드된 iframe : 내 페이지 안에 다른 사이트 페이지를 액자처럼 끼워 넣는 것

              <!-- 내 사이트 (mysite.com) -->
              <h1>강의 페이지</h1>
                                
              <iframe src="https://youtube.com/embed/영상ID">
                <!-- 여기 안은 youtube.com 의 세계 -->
                <!-- 유튜브 로그인 쿠키가 있어야 -->
                <!-- 광고 없이 보기, 저장 등이 가능 -->
              </iframe>
            
      • SameSite 속성 단독 사용

          Set-Cookie: session=xyz; Secure; HttpOnly
        
        • HTTPS 통신에서만 쿠키 전송
        • 네트워크 도청(Man-in-the-Middle) 공격 방어
        • HttpOnly 와 함께 쓰면 JS에서 쿠키 접근도 차단되어 XSS 방어까지 가능

    의미

    • 예전의 단순 cross-site POST CSRF는 과거보다 성공 조건이 까다로워짐
    • 하지만 여전히 끝난 공격이 아님
    • 특히 아래 경우는 여전히 위험

    위험이 남는 경우

    • 쿠키가 SameSite=None; Secure
    • 상태 변경이 GET
    • 로그인 직후의 예외 동작
    • 클라이언트 사이드 CSRF
    • 잘못된 CORS/Origin 처리
    • OAuth, SSO, 로그인 플로우의 state 검증 부재

CSRF 유형 (최신유형)

Client-Side CSRF

  • 기존 CSRF - 서버가 취약
  • Client-Side CSRF - 프론트엔드 JS가 취약

예를 들어 프론트 코드가 URL 파라미터를 읽어서 내부 API 요청을 만들어버리면, 공격자는 그 입력값을 조작해서 브라우저가 정상 토큰과 쿠키까지 붙인 요청을 보내게 만들 수 있음

취약 예시)

const api = new URLSearchParams(location.search).get("api");
fetch(api, {
  method: "POST",
  credentials: "include",
  headers: {
    "X-CSRF-Token": window.csrfToken
  }
});

위험한 이유

  • 토큰도 정상적으로 붙음
  • 쿠키도 정상적으로 붙음
  • 서버 입장에서는 “정상 SPA 요청”처럼 보임

즉, “CSRF 토큰이 있어도 프론트가 공격자 입력으로 요청을 만들면 뚫릴 수 있다”는 점이 중요

Login CSRF / OAuth CSRF

사용자를 공격자 계정으로 로그인시키는 형태

예)

  • 피해자가 쇼핑몰에 접속
  • 공격자가 만든 계정으로 로그인되게 유도
  • 피해자가 자기 정보라고 생각하고 결제/개인정보 입력
  • 실제론 공격자 계정에 데이터가 쌓임

OAuth/OIDC에선 state 검증 부재가 대표적


공격 페이로드를 볼 때 알아야할 것

  1. 인증 수단이 무엇인가
    • 쿠키 기반인지, Authorization 헤더 기반인지
      • 쿠키 기반 (CSRF 취약)

          로그인 시:
          서버 → 브라우저: Set-Cookie: session=abc123
                    
          이후 모든 요청:
          브라우저 → 서버: Cookie: session=abc123  ← 자동으로 붙음!
        

        브라우저가 자동으로 쿠키를 보내기 때문에, 공격자가 만든 페이지에서 요청해도 쿠키가 딸려감 (전통적인 웹사이트 로그인 방식)

      • Authorization 헤더 기반 (CSRF 안전)

          로그인 시:
          서버 → 클라이언트: { token: "eyJhbG..." }  ← JS가 직접 저장
                    
          이후 모든 요청:
          JS 코드가 직접: Authorization: Bearer eyJhbG...  ← 수동으로 붙여야 함
        

        JS가 직접 헤더를 붙여야 하므로, 공격자 사이트의 HTML form은 헤더를 임의로 추가 못함 → CSRF 공격 자체가 불가능 (현대적인 SPA + JWT 방식)

  2. 세션 쿠키의 SameSite 값은 무엇인가
  3. 대상 요청이 GET 인가 POST 인가
    • GET vs POST 방식의 차이
      • GET 요청 → 공격이 매우 쉬움

          <img src="https://......">
        

        이미지, 링크, scipt src 등 태그 하나로 공격이 가능

        피해자가 클릭을 안해도 페이지 로드만으로 실행

      • POST 요청 → 공격이 복잡하지만 그래도 가능

  4. 서버가 Origin / Referer 를 확인하는가

    브라우저는 “어디에서 왔는지 정보”를 헤더에 자동으로 담는다

    예)

     피해자가 evil.com 에서 mybank.com 으로 요청 시:
        
     Origin:  https://evil.com
     Referer: https://evil.com/hack.html
    
    • Origin → 요청의 출처 도메인만
    • Referer → 요청 직전 페이지의 전체 URL
  5. Sec-Fetch-Site를 확인하는가

    브라우저가 자동으로 붙여주는 헤더로, “이 요청이 어디서 시작됐는지”를 명확하게 알려줌

    Referer / Origin 보다 훨씬 정확하고 JS로 위조가 불가능

    의미 예시
    same-origin 완전히 같은 도메인 mybank.com → mybank.com/api
    same-site 같은 최상위 도메인 app.mybank.com → api.mybank.com
    cross-site 완전히 다른 도메인 ← CSRF는 여기 evil.com → mybank.com
    none 주소창 직접 입력, 북마크 -
  6. 프론트엔드가 공격자 입력으로 API 요청을 조립하는가

실습용 예시

1. Form POST 기반 CSRF

<!doctype html>
<html>
	<body onload="document.forms[0].submit()">
		<form action="http://localhost:3000/account/email" method="POST">
			<input type="hidden" name="email" value="attacker@lab.local">
		</form>
	</body>
</html>
  • 사용자가 localhost:3000 에 이미 로그인되어 있음
  • 브라우저가 해당 사이트의 세션 쿠키를 자동으로 붙임
  • 서버가 POST /account /email 요청에 대해 CSRF 토큰을 검사하지 않음
  • 서버는 “쿠키가 있으니 본인 요청이겠지”라고 잘못 판단

    → 로그인된 “요청” 인지 확인만 하고, 사용자가 직접 보낸 요청 인지 확인하지 않음

    → 취약점 : 자동 제출 form 자체가 아니라, 세션 쿠키만 보고 요청을 신뢰하는 서버 로직

2. GET 요청 기반 CSRF

<img src="http://localhost:3000/user/delete?id=10">
  • 브라우저는 이미지 로드를 위해 자동으로 GET 요청을 보냄
  • 사용자가 로그인된 상태면 관련 쿠키도 함께 전송될 수 있음
  • 서버가 GET /user/delete?id=10 같은 요청으로 실제 상태 변경을 수행함

    → 상태를 바꾸는 기능을 GET 으로 만든 설계 실수

    → GET 은 원래 조회용이어야 하는데, 삭제/변경 같은 동작을 넣어버렸기 때문

  • img, iframe, a, script 등 여러 HTML 요소가 GET 요청을 쉽게 발생시킴
  • 사용자는 클릭도 안했는데 요청이 나갈 수 있음

3. 자동 로그아웃 / 설정 변경형 CSRF

<a href="http://localhost:3000/account/logout">클릭</a>
<img src="http://localhost:3000/settings/toggle?darkmode=off">
  • 로그아웃이나 설정 변경이 단순 ”URL 호출”만으로 처리됨
  • 요청 출처 검증이 없음
  • 사용자가 해당 사이트에 로그인된 상태면 쿠키가 붙어서 정상 요청처럼 처리될 수 있음

    → 민감 기능이 너무 쉽게 호출되도록 설계

    → 서버가 “어디서 온 요청인지”, “사용자가 실제로 의도했는지” 확인하지 않음

4. 로그인 CSRF

<!doctype html>
<html>
	<body onload="document.forms[0].submit()">
		<form action="http://localhost:3000/login" method="POST">
			<input type="hidden" name="username" vlaue="attacker_demo">
			<input type="hidden" name="password" value="demo1234">
		</form>
	</body>
</html>
  • 로그인 요청에도 CSRF 방어가 없으면, 공격자가 자기 계정 정보로 피해자 브라우저에서 로그인 요청을 보낼 수 있음
  • 서버는 새 세션을 만들고 브라우저에 그 세션 쿠키를 심어줌
  • 피해자는 자기 계정이 아니라 공격자 계정으로 로그인된 상태가 됨

    → 개발자가 “로그인 전이니깐 CSRF 필요 없다”고 오해한 것

    → 하지만 로그인도 인증 상태를 바꾸는 중요한 상태 변경 요청

5. Client-Side CSRF

const api = new URLSearchParams(location.search).get("api");

fetch(api, {
  method: "POST",
  credentials: "include",
  headers: {
    "X-CSRF-Token": window.csrfToken
  }
});
  • 프론트엔드 코드가 공격자 제어 입력값(api)을 그대로 요청 대상으로 사용함
  • 브라우저는 credentials: “include” 때문에 쿠키를 포함
  • 앱이 보유한 정상 CSRF 토큰도 함께 전송할 수 있음
  • 서버 입장에서는 “정상 앱이 보낸 요청”처럼 보임

    → 서버보다 프론트엔드가 요청 대상을 안전하게 통제하지 못한 것

    → “요청 URL/동작은 앱 내부에서 고정되어야 하는데, 공격자 입력이 그 흐름에 끼어든 것”

  • 고전적 CSRF 방어인 토큰이 있어도 우회될 수 있기 때문에 최신 환경에서는 프론트엔드 설계까지 같이 봐야함!

6. JSON API

  • 예시 상황

    서버가 /api/profile 같은 JSON API를 제공

    인증은 여전히 세션 쿠키 기반

    서버가 Origin 검사나 CSRF 토큰 검증을 안 함

  • 가능한 이유

    많은 개발자가 “JSON이면 폼이 아니니 안전하다”고 착각함

    하지만 핵심은 요청 형식이 아니라 인증 방식

    쿠키 기반이면 브라우저가 자동 전송할 가능성이 있어 CSRF 관점 검토가 필요


방어 방법

CSRF 방어의 핵심은 로그인된 요청인지 확인하는 것이 아니라, 사용자가 실제로 의도한 요청인지 검증하는 것

위 공격 페이로드에서도 언급했지만 한번 더 정리하자면,

  1. CSRF 토큰 검증
    • 상태 변경 요청마다 예측 불가능한 토큰을 포함시키고 서버에서 검증
    • POST, PUT, PATCH, DELETE 같은 요청에 적용
    • 토큰이 있어도 프론트엔드가 공격자 입력으로 요청을 만들면 우회될 수 있으니, 토큰만 믿으면 안됨
  2. SameSite 쿠키 설정
    • 세션 쿠키에 SameSite=Lax 또는 엄격한 Strict를 적용
    • 외부 사이트에서 cross-site 요청 시 쿠키 전송을 줄여주지만 토큰을 대체하지는 못함
  3. Origin / Referer 검증
    • 민감 요청에서 출처가 자사 도메인인지 확인
    • 문자열 포함 검사 말고, 정확한 origin 비교를 해야함
    • 브라우저 환경에 따라 Origin이 없을 수 있어 Referer를 보조적으로 보는 경우도 많음
  4. Fetch Metadata 검사
    • Sec-Fetch-Site, Sec-Fetch-Mode 같은 헤더를 활용해 cross-site 상태 변경 요청을 차단
    • 최신 브라우저 환경에서 실무적으로 매우 유용한 추가 방어
  5. 상태 변경 기능에 GET 금지
    • GET 은 조회 전용으로만 사용
    • 삭제, 수정, 로그아웃, 결제 같은 동작은 모두 POST 계열로 분리해야 함
  6. 민감 기능에 재인증/추가 확인
    • 비밀번호 변경, 결제, 계정 삭제는 비밀번호 재입력, OTP, WebAuthn 같은 추가 검증을 둠
    • CSRF가 일부 우회되어도 피해를 줄일 수 있음
  7. 프론트엔드 요청 흐름 통제
    • URL 파라미터나 외부 입력으로 API 경로, 매서드, 액션을 직접 결정하지 않도록 설계
    • SPA에서는 이 부분이 요즘 특히 중요

현재 실무에서 중요한 포인트

  • SameSite만으로 끝났다고 생각하면 안됨
    • 예전보다 고전적 CSRF는 줄었지만, 완전히 사라진게 아님
    • 쿠키 정책, 예외 흐름, 잘못된 서버 설계 때문에 여전히 발생
  • Client-Side CSRF 를 같이 봐야함
    • 요즘은 서버만이 아니라 프론트엔드 코드가 공격자 입력으로 요청을 조립하는지가 매우 중요
    • 토큰이 있어도 프론트가 잘못 짜여 있으면 정상 토큰을 붙여 공격 요청이 나갈 수 있음
  • Login CSRF 와 OAuth/OIDC state 검증도 중요함
    • 로그인 요청은 보호 안해도 된다고 생각하면 위험함
    • SSO, 소셜 로그인, 연동 기능에서는 state 검증 누락이 큰 문제로 이어질 수 있음
  • CORS 와 CSRF 를 혼동하면 안됨
    • CORS는 응답 읽기 제어이고, CSRF는 요청 위조 문제
    • CORS를 잘 설정해도 CSRF가 자동으로 해결되지는 않음
  • XSS 와 함께 봐야함
    • XSS가 있으면 CSRF 토큰 탈취나 정상 요청 위조가 가능해져 방어가 무너질 수 있음
    • 실무에서는 CSRF 방어와 XSS 방어를 같이 설계해야함