2. CSRF

(1) CSRF의 구조

1) 이해

CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)다른 사이트에서 피해자의 인증 권한을 도용해 서버에 요청을 보내는 공격 방식이다. 웹 애플리케이션의 사용자는 로그인 과정에서 세션 ID나 토큰 같은 인증 정보를 브라우저에 저장한다. 그리고 계정 권한이 필요한 요청을 서버로 보낼 때 이 정보를 사용한다. CSRF는 해당 인증 정보만 있다면 서버가 요청의 출처나 내용을 확인하지 않으므로 누구나 인가를 받을 수 있다는 취약점을 노린다.

2) 공격 시나리오 예시

[그림 2-1] CSRF의 구조[그림 2-1] CSRF의 구조
[그림 2-1] CSRF의 구조
이메일을 통한 개인정보 변경 시나리오
이메일을 통한 개인정보 변경 시나리오
  1. 피해자가 웹 사이트에 로그인하면 인증 정보(세션 ID, 토큰 등)가 브라우저 저장소에 저장됨
  1. 공격자는 피해자의 브라우저 인증 정보를 이용해 사이트 개인정보를 변경하는 악성 스크립트를 만듦
  1. 공격자는 위 스크립트를 웹 페이지나 프로그램으로 제작하고, 피해자에게 웹 페이지 링크나 프로그램 첨부파일이 포함된 이메일을 발송함
  1. 피해자가 CSRF 취약점이 있는 웹 사이트에 로그인한 상태로 공격자의 이메일을 열어 봄. 그리고 그 안의 링크를 클릭하거나 첨부파일을 다운로드 받아 실행함
  1. 피해자의 브라우저가 자동으로 악성 스크립트를 실행해, 피해자 모르게 서버로 요청을 전송함
  1. 서버는 요청에 포함된 인증 정보를 확인하여 요청자를 짐작하고, 요청을 정상적으로 처리함
  1. 결국 사이트 내 피해자의 개인정보는 공격자가 의도한 대로 변경됨
이 시나리오에서 공격자는 피해자의 인증 세션을 직접 탈취하지 않았다. 즉 CSRF는 피해자의 브라우저를 통한 간접적인 방식으로 피해자의 권한을 위조할 수 있는 방법이다.¹⁾ 이제 공격자는 피해자도 모르는 새에 은행 사이트에서의 잔고 이체, 커뮤니티 사이트에서 게시물 작성, 데이팅 사이트에서 프로필 악용 등 다양한 악의적인 행위를 할 수 있게 된다.

3) XSS와 비교

CSRF는 앞서 살펴본 XSS와 '피해자의 브라우저에서 특정 스크립트를 실행한다'는 점에서 같다. 하지만 두 공격의 목적과 방식에는 차이가 있다. XSS가 악성 스크립트 실행을 통해 직접적으로 민감 정보를 탈취하고 악용²⁾한다면, CSRF는 기존에 인증된 피해자의 세션을 통해 간접적으로만 행동할 수 있다. 때문에 일반적으로 XSS의 피해가 CSRF의 피해보다 심각하다.
또한 XSS가 클라이언트 측에서 사용자 입력값이 적절히 처리되지 않는 취약점을 이용한다면, CSRF는 서버 측에서 요청의 출처를 제대로 검증하지 않는 취약점을 노린다. 따라서 공격 방어도 XSS는 클라이언트, CSRF는 서버측의 역할이라 할 수 있다.
추가로, 두 공격 방식이 완전히 독립적이지만은 않다. XSS 취약점을 이용하여 CSRF 공격을 시도하는 복합적인 방법이 존재하는데, 이러한 복합 공격은 XSS의 직접적인 위험성과 CSRF의 은밀함을 결합하여 더욱 강력하고 탐지하기 어려운 위협이 된다. 이처럼 웹 보안에서 다양한 취약점들은 서로 연관되어 있어 각각을 개별적으로 대응해야 할 뿐 아니라 종합적인 보안 전략을 수립해야 한다.

¹⁾ 반대로 피해자의 세션 토큰이나 쿠키 등을 직접 탈취하여 피해자로 위장하는 공격 방법을 세션 하이재킹이라고 한다.
²⁾ XSS를 이용한 세션 하이재킹
 

(2) 대책 방법

CSRF 취약점의 핵심은 서버에서 요청의 출처를 철저히 확인하지 않고 처리하는 데 있다. 그러므로 CSRF 방어를 위해서는 서버 측에서 요청의 출처와 정당성을 검증하는 메커니즘을 구현해야 한다. 프론트엔드는 이 과정을 이해하고, 필요한 검증 정보를 요청에 포함시켜 서버를 보조하는 역할을 한다.

1) CSRF 토큰(동기화 토큰) 패턴

세션별 고유한 토큰을 생성해 유효성을 검증하는 방식으로 가장 일반적인 CSRF 방어법 중 하나이다.
사용자 로그인 시 서버는 세션¹⁾을 시작한다. 각 세션마다 서버는 랜덤 문자열인 CSRF 토큰을 생성, 저장하고 클라이언트에 전달한다. 이후 클라이언트 요청마다 이 토큰을 포함하도록 하면, 서버는 저장된 토큰과 요청의 토큰이 일치하는지 비교해 요청의 정상 여부를 가려낼 수 있다. 이 패턴은 서버와 클라이언트 토큰의 비교 과정 때문에 동기화 토큰 패턴이라고도 부른다.
<form action="/" method="POST"> <input type="hidden" name="CSRF_TOKEN" value="SEcretTOken%1234&5678*90" /> <!-- 다른 form 요소들... --> </form>
위와 같이 HTML에 토큰을 삽입하고 <form>을 전송한다면, 클라이언트는 요청에 자동으로 CSRF 토큰을 첨부할 수 있다. CSRF 토큰값은 세션마다 변경되므로 공격자는 토큰값을 알 수 없고, 유효한 CSRF 토큰이 첨부되지 않은 요청은 서버에서 차단된다.

2) Double Submit 쿠키 패턴

CSRF 토큰 패턴은 세션 동안 서버에 토큰을 유지해야 한다. 이 방식은 사이트 접속자가 많아 세션 정보가 커지면 서버에 부하가 될 수 있다. 이에 대한 대안으로 Double Submit 쿠키 패턴을 사용할 수 있다. 이 패턴에서 토큰 두개의 일치성을 검증하는 로직은 CSRF 토큰 패턴과 동일하지만, 두 토큰을 모두 서버가 아닌 브라우저에 저장한다는 점에서 서버의 부하를 줄인다.
사용자 로그인시 서버는 랜덤 문자열 토큰을 생성하고 HttpOnly 플래그²⁾를 설정하지 않은 쿠키에 넣어 클라이언트로 전달한다. 쿠키는 브라우저에 저장되며 이후 클라이언트 요청에 자동으로 포함된다. 개발자는 자바스크립트로 브라우저에서 이 쿠키의 토큰 값을 읽어와 요청의 본문이나 커스텀 헤더에 추가로 포함시켜야 한다. 그러면 서버는 쿠키와 요청에 포함된 토큰을 비교해 일치 여부를 검사한다. 이렇게 동일한 토큰을 쿠키와 요청 데이터에 이중으로 보내기 때문에 Double Submit(이중 전송) 쿠키 패턴이라고 한다.
브라우저에 저장된 쿠키는 SOP(동일 출처 정책)에 의해 다른 도메인에서는 접근할 수 없으므로 CSRF 공격에 안전하다. 하지만 XSS 공격은 동일 도메인에서 일어나므로 SOP의 제한을 받지 않으니 주의가 필요하다.

3) SameSite 쿠키 속성 사용

CSRF는 교차 사이트 요청을 통해 브라우저의 인증 정보를 악용하는 공격 방법이다. 인증 정보로 쿠키를 사용하는 애플리케이션의 경우, 쿠키의 SameSite 속성을 설정해 브라우저가 교차 사이트 요청에 쿠키를 포함시키지 않도록 제한할 수 있다.
/* 서버 측 코드 */ const express = require('express'); const session = require('express-session'); // express 앱의 세션 관리용 라이브러리 const app = express(); // 서버 앱 생성 app.use(session({ // 세션 미들웨어 설정 cookie: { sameSite: 'strict', // sameSite 속성 설정 secure: true // HTTPS 사용 } }));
SameSite 속성이 동일 사이트를 판단하는 기준은 eTLD+1(effective Top Level Domain+1)이다. 유효한 최상위 도메인(eTLD)과 그 서브도메인을 포함하므로, 예를 들어 static.example.comapi.example.com은 동일 사이트이고, cross-site.com는 그렇지 않은 것이다.
SameSite의 값으로 설정할 수 있는 옵션은 아래와 같다.
SameSite
의미
Strict
동일한 사이트 요청에서만 쿠키 포함
Lax(기본값)
GET 요청과 최상위 레벨 탐색에는 쿠키 포함. 그 외의 교차 사이트 요청에는 제한
None
사이트에 관계없이 모든 요청에 쿠키 포함

4) Origin 헤더 검사

서버에 들어온 요청의 Origin 헤더를 검사해서 출처의 신뢰성을 검사하는 방법이다. Origin 헤더는 클라이언트 요청시 브라우저에 의해 자동으로 설정되고 JavaScript로 조작할 수 없어 믿을 수 있는 정보를 제공한다.
/* 서버 측 코드 */ const express = require('express'); const app = express(); // 서버 앱 생성 app.use((req, res, next) => { // 출처 검증 미들웨어 설정 const allowedOrigins = ['https://example1.com', 'https://example2.com']; // 신뢰할 수 있는 출처 목록 const origin = req.headers.origin; if (allowedOrigins.includes(origin)) { next(); } else { res.status(403).json({ error: 'Forbidden - Invalid Origin' }); } });
이 방법은 구현이 가장 간단하지만, 신뢰할 수 있는 출처 목록을 지속적으로 관리해야 한다는 단점이 있다. 또한 CDN이나 프록시 서버를 사용하는 경우는 고려하기 까다로워 주로 다른 CSRF 방어 기법의 보조 수단으로 사용된다.

¹⁾ 사용자와 서버 간의 연결, 또는 그 연결 유지 상태를 서버에 기록한 것
²⁾ 쿠키 생성시 HttpOnly 플래그를 설정하면 쿠키에 HTTP(S)로만 접근이 가능하고 스크립트 언어로는 접근할 수 없게 된다. 반대로 이 플래그를 설정하지 않는다면 개발자가 자바스크립트로 접근할 수 있다.