관찰된 패턴 · 2026년 5월 12일 · 8분
카카오로 로그인한 사람의 이메일, 실제로 검증된 상태일까
카카오 OAuth 응답은 이메일이 검증되지 않은 상태로도 그대로 도착할 수 있습니다. AI가 만들어준 인증 코드를 그대로 올리기 전, 응답 필드 세 가지를 확인하는 글입니다.
요약
- 카카오 OAuth 응답에는
email외에is_email_valid,is_email_verified,email_needs_agreement가 따로 있습니다. 이메일 필드가 있다는 것과 그 이메일이 검증된 상태라는 것은 다른 문제입니다. - AI 코딩 도구가 만들어준 코드와 라이브러리 default 모두 이 세 필드를 기본 검사하지 않는 경우가 많습니다.
profilecallback에서 가드가 빠지면 미검증 이메일이 정식 이메일처럼 저장됩니다. - 같은 이메일로 다른 provider에서 가입한 계정이 있고 서비스가 이메일 기반 자동 linking을 한다면, 기존 계정 위에 다른 사람이 합쳐지는 경로가 열립니다.
카카오 로그인 흐름을 한 번 통과시키고 응답을 그대로 들여다보면, 이메일은 들어 있지만 그 이메일의 상태는 별도의 필드에 따로 적혀 있습니다. 라이브러리 default가 이 별도의 필드를 기본 확인하지 않는다는 점이 이 글의 출발점입니다.
AI 코딩 도구로 카카오 로그인을 빠르게 붙이면, profile callback의 default 구현이 그대로 들어가기 쉽습니다. 여기서 가드가 빠지면 검증되지 않은 이메일이 정식 이메일처럼 사용자 record에 저장되고, 같은 이메일을 쓰는 다른 사용자 계정과 충돌·합쳐질 위험이 생깁니다.
카카오 OAuth 응답 안에 무엇이 있는가
카카오 OAuth 응답의 kakao_account 객체에는 email 외에 그 이메일의 상태를 설명하는 세 필드가 같이 있습니다. email 필드는 이메일 값만 알려주고, 그 이메일의 상태는 다음 세 항목에 나뉘어 있습니다.
is_email_valid— 형식이 유효하고 활성 상태인가is_email_verified— 카카오 측에서 소유가 확인되었는가email_needs_agreement— 이메일 공개 동의 이전인가 (동의 이전이면 응답에email자체가 포함되지 않을 수 있음)
kakao_account 안에는 이메일 값 외에 그 이메일의 상태를 설명하는 세 필드가 따로 있습니다.{
"id": 12345678,
"kakao_account": {
"email": "victim@example.com",
"is_email_valid": true,
"is_email_verified": false,
"email_needs_agreement": false
}
}- email — 어떤 이메일 값인가
- is_email_valid — 형식이 유효하고 활성 상태인가
- is_email_verified — 카카오 측에서 소유가 확인되었는가
- email_needs_agreement — 이메일 공개 동의 이전인가
is_email_verified가 false면, 이메일 값은 있지만 카카오 측 검증이 완료되지 않은 상태입니다.세 필드가 모두 검증 통과 상태일 때만 카카오 측에서 현재 소유가 확인된 이메일입니다. 그 외 케이스는 이메일 값이 응답에 있어도 그대로 정식 이메일로 받으면 안 되는 경우입니다.
profile callback default에서 빠져 있는 것
Auth.js의 Kakao provider default profile 매핑은 email: profile.kakao_account?.email처럼 이메일 값만 그대로 가져옵니다. is_email_verified와 email_needs_agreement는 확인하지 않습니다. 결과적으로 어떤 상태의 이메일이든 그대로 user.email로 저장됩니다.
직접 OAuth를 구현했을 때도 자주 같은 곳에서 빠뜨립니다. 카카오 /v2/user/me 응답 본문을 받아 response.kakao_account.email만 꺼내 쓰는 패턴이 그 모양입니다.
AI 코딩 도구가 만들어준 코드도 이 default 위에 그대로 쌓이기 쉽습니다. “카카오 로그인 붙여줘” 같은 짧은 지시에는 검증 필드 확인이 명시되지 않으므로, 만들어진 코드도 자연스럽게 그 한 줄에 머무릅니다.
왜 이게 계정 takeover 경로가 되는가
구체적인 시나리오 하나로 풀어 봅니다.
- victim이 서비스에 Google OAuth로 가입해두었습니다.
victim@example.com, Google 측email_verified: true상태입니다. - 공격자가 카카오 계정을 하나 만듭니다. 카카오는 전화번호 기반 가입이 기본이라, 카카오 계정 자체는 공격자의 번호로 만들고 그 다음 카카오 계정 설정에서 이메일 자리에
victim@example.com을 입력합니다. - 카카오는 해당 주소로 인증 메일을 보냅니다. 인증 메일은 victim의 받은 편지함으로 가지만 victim은 자신이 카카오에 가입한 적이 없으므로 무시하거나 스팸 처리할 수 있습니다. 인증이 완료되지 않은 채 공격자의 카카오 계정에는 이메일 자리에 victim의 주소가 등록되어 있되 미검증인 상태가 남습니다 —
is_email_verified: false. - 공격자가 자기 카카오 계정으로 서비스의 카카오 로그인 버튼을 누릅니다. 서비스 백엔드의
profilecallback에 카카오 응답이 도착하고, 거기엔email: "victim@example.com"가 들어 있되is_email_verified: false가 함께 적혀 있습니다. - 서비스가
is_email_verified를 확인하지 않고 이메일 기반 자동 account linking을 하면, 공격자의 카카오 identity가 victim의 기존 Google-기반 계정 위에 연결됩니다. 이후 공격자는 카카오 로그인만으로 victim의 데이터·이력에 접근합니다.
email 필드만 꺼내 쓰고 is_email_verified를 확인하지 않으면, 미검증 이메일이 정식 이메일처럼 다음 단계로 전달됩니다.이 경로의 핵심은 카카오에 이메일을 등록하는 것과 그 이메일을 검증받는 것이 분리되어 있다는 점입니다. 공격자가 등록만 하고 검증은 하지 않은 상태에서도 OAuth 응답에는 해당 이메일 값이 그대로 노출됩니다. 서비스 쪽에서 검증 필드를 확인해야 이 경로가 닫힙니다.
응답 안에서 무엇을 확인할 것인가
점검 방법은 단순합니다. profile callback에서 세 필드를 모두 확인합니다.
email_needs_agreement === false— 이메일 공개 동의 이전이 아님 (동의가 이미 완료된 상태)is_email_valid === true— 카카오 측 형식·활성 검증 통과is_email_verified === true— 카카오 측에서 소유가 확인됨
셋 중 하나라도 만족하지 않으면 그 이메일을 정식 이메일로 받지 않습니다. 처리 방식은 두 갈래로 나뉩니다.
- 거절.
profilecallback에서 미검증 이메일을 가진 응답은 로그인 자체를 거절하고, 사용자에게 카카오 계정 설정에서 이메일 인증을 완료한 뒤 다시 시도하도록 안내하는 방식. - 분리 저장 + 별도 검증. 응답은 받되 user record의
email과emailVerified를 두 필드로 나눠 저장하고, 미검증 상태에서는 검증 메일을 서비스가 직접 보내 이메일 소유를 확인하는 방식. 이때까지는 그 이메일을 다른 계정과 연결하는 식별자로 쓰지 않습니다.
어느 쪽을 택하든 핵심은 같습니다 — 미검증 이메일이 정식 이메일처럼 다른 흐름(account linking, 비밀번호 재설정, 알림 발송 등)에 섞이지 않게 분리합니다.
account linking 정책의 영향
위 §3 시나리오가 성립하는 마지막 조건은 서비스가 이메일이 같으면 같은 계정으로 합친다는 정책을 자동으로 적용한다는 가정입니다. 이 가정이 깔려 있으면 미검증 이메일 가드가 빠지는 순간 takeover 경로가 열립니다.
안전한 linking 정책은 세 줄로 정리할 수 있습니다.
- 동일 provider 내 linking만 자동. 같은 provider의 같은 user id면 같은 계정으로 본다는 기본만 자동 적용. Google의 같은
sub, 카카오의 같은 user id처럼 provider가 보장하는 안정 식별자를 씁니다. - cross-provider는 명시적 link만.이미 로그인된 세션에서 사용자가 “다른 provider 연결하기” 같은 동작을 직접 했을 때만 추가 provider를 같은 계정에 묶습니다. 응답 안의 이메일 값이 같다는 이유만으로 자동 묶지 않습니다.
- 미검증 이메일은 비교 대상에서 제외. 자동·수동 어느 쪽이든 검증 통과한 이메일끼리만 비교합니다. 미검증 응답이 들어오면 새 계정으로 분리하거나 위 §4의 거절 방식으로 처리합니다.
linking 정책을 단순히 “이메일 같으면 합치기”로 둔 경우라면, 미검증 이메일 가드와 linking 정책을 같이 손보는 편이 자연스럽습니다. 한쪽만 고치면 다른 쪽이 다음 사고의 경로로 남습니다.
다른 OAuth provider는 어떻게 답하는가
provider마다 응답 형태가 다릅니다. 공통점은 OAuth 통과 = 이메일 검증 완료라는 가정이 어디서도 안전 default가 아니라는 점입니다.
- Google.
email_verified가 default 응답 본문에 포함됩니다. 대부분true지만 일부 workspace 계정에서false가 가능합니다. 받는 쪽에서true를 명시적으로 확인하는 편이 안전합니다. - GitHub. 사용자 이메일 목록을
/user/emails로 받아 각 항목의verified필드를 확인합니다. primary 이메일이라도 미검증일 수 있습니다. - 네이버. 이메일 동의가 선택 항목이라 응답에 이메일이 빠질 수 있고, 미검증 상태도 가능합니다. 카카오와 비슷한 패턴입니다.
- Apple. 첫 로그인에서만 이메일이 응답에 포함되고 이후엔 빠지는 패턴이라, 첫 응답을 처리하는 시점에 검증 상태와 어디에 저장할지를 같이 결정해야 합니다.
어느 provider든 응답 필드 검증을 default로 두고, 검증 필드를 profilecallback에서 명시적으로 가드합니다. “OAuth 통과 = 이메일 검증”이라는 가정은 피합니다.
Preflight에서 이걸 어떻게 봐 왔는가
인터뷰에서 카카오 OAuth 사용을 답하면, 점검 항목 안에 이 항목이 들어옵니다. AI 코딩 도구로 빠르게 붙인 카카오 로그인에서 가장 자주 빠뜨리는 한 단계로 관찰됩니다 — 응답의 is_email_verified를 가드하지 않고 이메일 값만 그대로 꺼내 쓰는 경우입니다.
이 영역은 자동 점검만으로는 부족합니다. account linking 정책, 미검증 이메일을 받았을 때의 후속 흐름, 검증 메일 발송 인프라까지 같이 봐야 하므로, 출시 직전 한 번 더 확인할 영역으로 분류합니다.
자신의 프로젝트에서 카카오 로그인을 붙였다면, 점검을 한 번 돌려 profile callback이 검증 필드 세 항목을 보고 있는지부터 확인하는 편이 빠릅니다.