공격자 계정으로 발급된 OAuth callback을 피해자 브라우저에서 재생하자 피해자 세션이 공격자 연결 계정으로 로그인됐습니다. 두 브라우저 세션으로 session swapping을 확인했습니다.
Taegu Ha · NamelessMC · CWE-302 · CVSS 5.4
2026년 6월 2일 공개 · NamelessMC 2.2.5에서 수정 · 공식 기록
한눈에 보기공격 흐름취약점 개요조사 대상원인 분석재현 및 검증판단 변화계정 탈취Code 검증Cookie 분리영향과 수정확인한 범위제외한 범위수정분석 기준과 증거 보존관련 파일2026-08-23 최종 감사느낀 점
한눈에 보기
NamelessMC의 OAuth callback은 provider와 code는 처리했지만, 로그인 시작을 victim session에 묶는 server-side
state 검증이 없었습니다. 공격자가 만든 callback을 victim에게 열어 attacker-linked account로 로그인시킬 수 있었습니다.구분 | 확인 내용 |
프로젝트·컴포넌트 | NamelessMC OAuth login callback |
공격 입력 | 공격자가 자신의 OAuth flow에서 획득한 유효 callback·code |
필요 조건 | 피해자가 공격자가 준비한 callback URL을 열어야 함 |
취약 지점 | 예측 불가능한 one-time state를 victim session과 검증하지 않음 |
검증 결과 | victim browser session이 attacker-linked application account로 로그인됨 |
영향 | login CSRF 및 account confusion |
수정 원칙 | code exchange 전 session-bound one-time state를 생성·검증·폐기 |
공격 흐름
검증 결론: OAuth provider가 code를 검증해도 browser session과 authorization response의 결속이 없으면 login CSRF가 성립함을 확인했습니다.
취약점 개요
OAuth authorization code는 provider에서 정상 발급된 값이더라도, 그 로그인을 시작한 browser session과 callback을 묶어야 합니다. 일반적으로 무작위
state를 session에 저장하고 callback의 state와 비교합니다.NamelessMC callback은
provider, code, session의 oauth_method를 확인했지만 state를 서버 측에서 검증하지 않았습니다. 공격자가 자신의 OAuth 로그인을 시작해 얻은 callback URL을 피해자의 로그인 흐름에 주입하면 피해자 browser가 공격자 연결 계정으로 인증됐습니다.공격자가 준비하는 값 | 자신의 provider authorization code가 든 callback URL |
누락된 검증 | session에 저장한 state와 callback state 비교 |
피해 결과 | 피해자 browser session이 공격자 계정으로 로그인 |
분류 | login CSRF / session swapping |
조사 대상
OAuth 구현에서는 code exchange가 성공하는지만 보지 않고 authorize 요청을 시작한 session과 callback을 묶는 값을 추적했습니다. login, register, account linking이 같은 callback을 공유하는지도 확인했습니다.
modules/Core/pages/oauth.php는 oauth_method가 존재하는지는 검사했지만 state를 읽거나 비교하는 코드가 없었습니다. provider의 getAccessToken()에는 callback code만 전달됐습니다.한 browser가 시작한 OAuth transaction의 callback을 다른 browser session이 받아도 로그인되는가?
원인 분석
공격자 session A에서 Discord OAuth를 시작하고, provider가 공격자 계정에 대해 돌려준 callback URL을 로그인 완료 전에 보존했습니다. 별도 피해자 session B에서도 OAuth login을 시작해
oauth_method=login 상태를 만들었습니다.session B의 browser를 session A에서 얻은 callback URL로 이동시키자 server는 state 소유자를 확인하지 않고 code를 교환했습니다. 그 결과 session B가 공격자 Discord와 연결된 NamelessMC 계정으로 로그인됐습니다.
oauth.php · state 검증 없이 code를 교환한 흐름
if (!isset($_GET['provider'], $_GET['code'])) { ... } if (!Session::exists('oauth_method')) { ... } $provider = getProvider($_GET['provider']); $token = $provider->getAccessToken('authorization_code', [ 'code' => $_GET['code'] ]); // callback state와 session state 비교 없음
입력에서 영향까지의 경로
- 공격자가 자신의 OAuth 로그인을 시작합니다.
- 공격자 계정용 callback URL을 보존합니다.
- 피해자도 별도 session에서 OAuth login을 시작합니다.
- 공격자가 준비한 callback을 피해자 browser에서 엽니다.
- 피해자 session이 공격자 연결 계정으로 인증됩니다.
재현 및 검증
cookie jar가 분리된 두 browser profile을 사용했습니다. 공격자와 피해자 session의 cookie가 섞이면 단순 session 공유처럼 보일 수 있어 callback URL만 session 사이에 이동시켰습니다.
정상 callback, 임의 code, 공격자 callback 재생을 나눴습니다. 임의 code는 provider 교환에서 실패했지만 유효한 공격자 code는 피해자 session에서 받아들여졌습니다. 따라서 provider 검증 부재가 아니라 transaction binding 부재로 판단했습니다.
# Session A: attacker OAuth 시작 → callback URL 보존 # Session B: victim OAuth 시작 # Session B browser에서 Session A callback URL 열기
검사 | 입력·조건 | 관찰 | 의미 |
정상 흐름 | A가 시작한 callback을 A에서 완료 | A 계정 로그인 | OAuth 설정 정상 |
잘못된 code | 임의 authorization code | provider 교환 실패 | code 자체 검증은 provider가 수행 |
교차 session | A callback을 B에서 열기 | B가 A 연결 계정으로 로그인 | transaction binding 없음 |
두 browser session에서 확인한 상태 변화
Session A OAuth identity: attacker-discord callback: /oauth?provider=discord&code=A_CODE&state=A_STATE Session B starts OAuth login opens A callback resulting NamelessMC account: attacker-linked account
판단 변화
계정 탈취
피해자가 공격자 계정으로 로그인된다는 결과를 피해자 계정 탈취로 표현할 수는 없었습니다. 공격자가 피해자 계정의 권한을 얻은 것이 아니라 피해자 browser의 인증 주체가 바뀐 것입니다. login CSRF와 session swapping으로 범위를 바로잡았습니다.
Code 검증
callback에 유효한 provider code가 필요하므로 아무 문자열로 로그인되는 취약점은 아니었습니다. 임의 code 대조군이 실패하는 것을 확인해 OAuth provider 인증은 동작하고, 빠진 것은 요청과 응답의 session binding이라는 점을 분리했습니다.
Cookie 분리
한 browser에서 계정만 바꾸며 재현하면 cookie 오염을 배제하기 어렵습니다. 두 profile과 두 cookie jar를 사용한 뒤 callback URL만 옮겨 가설을 단순화했습니다.
영향과 수정
공격자는 피해자의 browser를 자신이 준비한 callback으로 유도해 피해자가 공격자 소유 계정으로 로그인한 상태에서 작업하게 만들 수 있습니다. 피해자가 입력한 개인정보나 이후 수행한 작업이 공격자 계정에 남을 수 있습니다.
확인한 범위
- 서로 분리된 두 session 사이의 callback 재생
- 피해자 session의 공격자 연결 계정 로그인
- 유효 code가 필요하다는 조건
제외한 범위
- 피해자 NamelessMC 계정 탈취
- provider authorization code 위조
- OAuth provider 자체의 취약점
수정
2.2.5에서는 OAuth 시작 시 예측 불가능한 state를 session에 저장하고 callback에서 일치 여부를 확인한 뒤 code를 교환하도록 수정됐습니다. state는 사용 후 폐기해 재사용도 막아야 합니다.
분석 기준과 증거 보존
항목 | 최종 감사 결과 |
소스 기준 | NamelessMC checkout ba6d81b77cd21358da091ea2941dd965ad7ae3b1 |
원본 검증 | OAuth 시작·callback 경로의 state 생성·검증 여부 추적, attacker·victim session 분리 flow 확인 |
GitHub 보존 상태 | Public 저장소 main의 감사 commit c04cacf; evidence 4개와 root manifest.json에 size·SHA-256 고정 |
무결성 | 사례별 SHA256SUMS 검증 통과, root manifest의 모든 file hash와 실제 파일 일치, README 상대 링크 확인 완료 |
보존 경계·제약 | 실제 OAuth token, client secret, 계정 cookie와 원본 session은 제외 |
감사 완료 | 2026-08-23 · Docker/로컬 원본 대조 후 GitHub push 완료 |
관련 파일
2026-08-23 최종 감사
조사 당시 callback 소스와 공개 advisory·CVE 레코드를 포함했습니다. 실제 OAuth code와 session cookie는 포함하지 않았습니다.
- oauth.php — state 검증이 빠진 조사 당시 callback handler
- github-advisory.json — 두 session 재현 절차가 포함된 공개 기록
- cve-record.json — CVEProject 공개 레코드
느낀 점
OAuth에서 provider가 code를 검증한다는 사실과 우리 애플리케이션이 그 callback의 주인을 검증한다는 사실은 별개였습니다. 외부 인증을 사용해도 local session binding은 애플리케이션 책임이라는 점이 분명해졌습니다.
영향 명칭을 고치는 과정도 중요했습니다. ‘로그인이 바뀐다’를 곧바로 계정 탈취로 부르지 않고 어느 쪽의 session과 account가 바뀌는지 도식화하자 login CSRF의 실제 위험과 한계를 더 정확히 설명할 수 있었습니다.