OAuth state 파라미터는 CSRF 공격을 어떻게 막는가
- 1.OAuth state 파라미터는 CSRF 공격을 어떻게 막는가← 현재

1. 들어가며
OAuth 2.0으로 소셜 로그인을 구현하다 보면 state라는 파라미터를 자연스럽게 마주하게 됩니다. 스펙 문서에는 "CSRF 공격 방어용"이라고만 짧게 설명되어 있는 경우가 많은데, 정작 "OAuth 흐름 안에서 CSRF 공격이 정확히 어떻게 일어나는지", 그리고 "state가 그중 어느 지점을 막는지"는 명확히 그려지지 않을 때가 많습니다. 이 글에서는 OAuth 인증 플로우를 먼저 복기한 뒤, 그 위에서 실제로 벌어지는 CSRF 공격의 단계를 하나씩 따라가 보고, 마지막으로 state가 이를 어떻게 차단하는지 정리합니다.
2. 본격적인 내용에 앞서: 인증 플로우 복기
CSRF 공격을 이해하려면 먼저 정상적인 OAuth Authorization Code 플로우를 명확히 알고 있어야 합니다. 등장하는 주체는 세 가지입니다.
- 앱(클라이언트): 실제 서비스 서버
- 사용자 브라우저: 인증서버와 앱 사이에서 리다이렉트를 중계
- 인증서버(Authorization Server): Google, GitHub 같은 OAuth Provider 이를 기준으로 인증 플로우는 다음 여섯 단계로 정리됩니다.
- 앱이 사용자 브라우저를 통해 인증서버로 인증을 요청합니다.
- 인증서버가 사용자에게 로그인 및 동의를 받습니다.
- 인증서버가 사용자 브라우저를 통해 앱의 redirect_uri로 authorization code를 발급합니다.
- 사용자 브라우저가 이 code를 앱의 콜백 엔드포인트에 전달합니다.
- 앱 서버가 이 code를 인증서버에 제출해 액세스 토큰으로 교환합니다.
- 앱이 액세스 토큰으로 사용자 정보를 조회하고, 해당 계정 정보를 현재 세션에 연결해 로그인 처리를 완료합니다. 이 여섯 단계에서 가장 중요한 전제는, "code를 발급받은 사람"과 "그 code를 앱에 제출하는 사람"이 항상 동일할 것이라는 암묵적 가정입니다. 지금부터 살펴볼 CSRF 공격은 바로 이 전제를 깨뜨리는 것에서 출발합니다.
3. CSRF 공격은 이 플로우 안에서 어떻게 일어나는가
3.1. 공격자가 3단계까지 진행해 자신의 code를 발급받습니다
공격자는 자신의 브라우저로 정상적인 로그인 절차(1~3단계)를 밟습니다. 이 과정 자체에는 아무런 조작이나 트릭이 없습니다. 그 결과 공격자는 자기 자신의 계정에 대한, 지극히 정상적인 authorization code를 손에 넣습니다.
공격자 브라우저 → 앱: 로그인 시도 (1단계)
앱 → 인증서버로 리다이렉트 (1단계)
공격자 → 인증서버 로그인 (2단계, 자기 계정)
인증서버 → 앱의 redirect_uri로 code 발급 (3단계)
공격자는 여기서 4단계(콜백을 앱에 제출하는 것)를 자기 손으로 실행하지 않고, ?code=ATK_CODE가 담긴 이 콜백 URL 자체를 확보하는 데서 멈춥니다.
3.2. 확보한 URL을 사용자(공격 대상자)에게 보냅니다
공격자는 이 URL을 피해자가 열람하도록 유도합니다. 이메일, 메신저, 게시판 등 다양한 경로로 이 링크를 전달합니다.
3.3. 사용자가 이 URL을 클릭하게 만듭니다
피해자는 이 링크가 평범한 링크라고 생각하고 클릭합니다. 여기서 피해자는 자신이 OAuth 콜백을 처리하고 있다는 사실을 전혀 인지하지 못합니다.
3.4. 클릭으로 인해 사용자가 공격자의 code로 액세스 토큰을 받게 됩니다
링크 클릭으로 피해자의 브라우저는 자동으로 GET /callback?code=ATK_CODE 요청을 앱 서버에 전송합니다. 앱 서버는 이 code를 인증서버에 제출해 액세스 토큰으로 교환합니다(5단계).
참고로 사용자가 URL을 "클릭"하게 만드는 방법 외에도, 이 4단계를 유발하는 수단은 다양합니다.
- 클릭 없이 페이지 로딩만으로 요청이 발생하는
<img src="...">,<iframe src="...">태그 - JavaScript를 이용한 강제 리다이렉트
- 사이트 자체의 XSS 취약점을 경유한 요청 발생
- 오픈 리다이렉트 취약점을 경유한 위장 리다이렉트 수단은 다양하지만 목적은 하나입니다. 피해자의 브라우저가 공격자의 code를 앱에 대신 제출하게 만드는 것입니다.
3.5. 액세스 토큰으로 계정 정보를 가져올 때, 사용자의 세션이 공격자 계정에 연결됩니다
앱은 방금 받은 액세스 토큰으로 사용자 정보를 조회합니다(6단계). 이 토큰은 공격자의 code로 발급된 것이므로, 조회되는 계정 정보는 공격자의 계정입니다.
문제는 앱이 "지금 이 요청을 보낸 세션(피해자의 세션)"과 "이 계정 정보(공격자 계정)"를 그대로 연결시켜 버린다는 점입니다. 앱 입장에서는 "콜백을 제출한 세션 = code를 발급받은 사람"이라는 가정이 항상 성립해왔기 때문에, 이 둘이 분리되어 있다는 걸 감지할 방법이 없습니다.
3.6. 결과적으로 사용자는 공격자의 계정과 연결된 세션으로 사이트를 이용하게 됩니다
세션 쿠키 자체가 바뀌는 것이 아닙니다. 피해자가 원래 갖고 있던 세션 ID는 그대로 유지되지만, 앱 서버 안에서 그 세션이 매핑하는 로그인 대상(계정)만 공격자의 것으로 바뀝니다.
공격 전: 피해자_세션 → (로그인 안 됨)
공격 후: 피해자_세션 → 공격자 계정
4. 공격의 결과: 사용자가 남긴 흔적이 공격자에게 넘어갑니다
이 상태에서 피해자는 자신이 공격자의 계정으로 로그인되어 있다는 사실을 전혀 눈치채지 못한 채 사이트를 이용합니다. 결제 정보를 등록하거나, 배송지·전화번호 같은 개인정보를 입력하면, 이 정보는 처음부터 공격자 계정 소속으로 저장됩니다.
이후 공격자가 자신의 원래 브라우저로 자기 계정에 정상 로그인하면, 피해자가 남겨둔 이 흔적을 그대로 열람하고 이용할 수 있게 됩니다. 만약 서비스에 "기존 로그인 계정에 소셜 계정 연동하기" 기능이 있고 이 기능의 콜백에도 동일한 취약점이 있다면, 피해자의 기존 계정 자체가 공격자의 소셜 계정에 연동되어 버리는 훨씬 심각한 결과로 이어질 수도 있습니다.
5. state 파라미터는 이 과정을 어떻게 방지하는가
5.1. 로그인 시도 시점에 앱 서버가 사용자의 세션에 state를 세깁니다
사용자가 로그인을 시도하면(1단계), 앱 서버는 그 시도 하나만을 위한 무작위 값 state를 새로 생성합니다. 이 값은 앱 서버 자신이 관리하는 사용자의 세션 저장소에 기록됩니다. 여기서 중요한 점은, state는 계정에 영구히 귀속되는 값이 아니라 매 로그인 시도마다 새로 생성되고 검증 후 소멸되는 1회성 값이라는 것입니다.
5.2. 앱이 인증서버로 state를 실어 보냅니다
앱은 이 state 값을 인증 요청 URL에 실어 인증서버로 전송합니다.
GET https://인증서버/authorize?...&state=xY7k...
5.3. 인증서버는 받은 state값과 code를 함께 반환합니다
인증서버는 state의 의미를 알지도, 검증하지도 않습니다. 그저 받은 값을 그대로 다시 돌려주는 역할만 합니다.
GET /callback?code=발급된code&state=xY7k...
5.4. 돌아온 state를 세션에 저장된 state와 비교합니다
콜백이 도착하면 앱 서버는 두 값을 비교합니다.
"콜백으로 돌아온 state" == "이 세션에 저장해둔 state"?
일치하면 그제서야 code를 인증서버에 제출해 액세스 토큰을 발급받습니다. 검증이 끝나면 세션에 저장해둔 state 값은 즉시 소멸시켜, 동일한 값이 재사용되는 것을 막습니다.
5.5. 공격자는 이 검증을 통과할 수 없습니다
공격자는 자신의 브라우저에서 자신의 state를 생성해 자신의 세션에 저장했을 뿐, 피해자의 세션에는 어떤 값도 써넣을 수 없습니다. 피해자가 공격자의 URL을 클릭하면, 피해자의 브라우저는 자신의 세션 ID만 자동으로 함께 실어 보내고, 공격자가 만든 state는 콜백 파라미터에 담겨 오지만 정작 앱 서버가 피해자의 세션에서 대조할 값은 다음 둘 중 하나입니다.
- 피해자가 애초에 로그인을 시도한 적이 없다면, 세션에 저장된 state 자체가 존재하지 않습니다.
- 피해자가 마침 별도로 로그인을 시도 중이었다면, 세션에 저장된 state는 피해자 자신의 시도에서 생성된 전혀 다른 값입니다. 어느 경우든 공격자가 보낸 state와 일치할 수 없습니다. 결국 검증에 실패해 토큰 교환 자체가 거부되고, CSRF 공격이 차단됩니다.
6. 마무리
정리하면, OAuth Login CSRF는 정보를 훔치는 공격이 아니라 공격자 자신의 authorization code를 피해자에게 떠안겨, 피해자의 세션을 공격자의 계정에 연결시키는 공격입니다. state 파라미터는 "이 콜백이 정말 내가 시작한 요청에 대한 응답이 맞는가"를 인증 요청 시점에 미리 세션에 심어둔 무작위 값으로 증명하는 장치이며, 공격자는 피해자의 세션에 이 값을 써넣을 방법이 없기 때문에 검증을 통과할 수 없습니다. 이런 원리로 state는 OAuth 플로우 안에서 CSRF 공격을 방어합니다.