Загружаем каталог…
Загружаем каталог…
"소셜 로그인은 당연히 공급자(Google, Kakao, Naver)가 이메일을 인증해 줬겠지?" 이 안일한 가정 하나가 서비스 전체의 관리자 계정 탈취(Account Takeover) 취약점으로 이어질 수 있습니다. 실제 서비스 운영 중 코드 리뷰를 진행하다가 발견한 치명적인 OAuth 계정 선점 및 탈취 취약점 과, 이를 프로덕션 무중단 배포로 안전하게 방어해 낸 과정을 공유합니다. 🚨 1. 문제의 발단: 어떻게 계정이 털릴 수 있었을까? 기존 시스템의 소셜 로그인 로직은 일반적인 웹 서비스들과 비슷하게 구성되어 있었습니다: 사용자가 카카오/구글/네이버로 로그인 요청. IdP(공급자)가 인증 후 사용자 정보(User Info)를 넘겨줌. 백엔드의 OAuth2SuccessHandler 가 넘어온 email 문자열 을 읽음. DB에서 해당 이메일을 가진 회원이 있는지 조회: 있으면? ➡️ 기존 회원으로 판단하고 즉시 로그인(JWT 발급) 없으면? ➡️ 신규 소셜 회원가입 처리 [공격 시나리오] 1. 피해자(또는 관리자)의 이메일: admin@company.com (일반 이메일/비밀번호 가입 계정) 2. 공격자가 카카오나 타 플랫폼에서 admin@company.com을 임의로 입력하여 소셜 계정 생성 (이메일 인증 미완료 상태) 3. 공격자가 우리 서비스에서 "카카오 로그인" 클릭 4. 우리 서버는 넘어온 email이 "admin@company.com"이라는 이유만으로 기존 관리자 계정의 세션을 그대로 발급해버림! ➡️ 💥 비밀번호 없이 관리자 계정 로그인 성공 (Account Takeover) 더 심각했던 점은, 아직 가입하지 않은 피해자의 이메일로 공격자가 먼저 소셜 가입을 해두면, 나중에 피해자가 정식 가입을 하려고 해도 계정이 선점당하는 문제도 존재했습니다. 🔍 2. 근거와 원인 분석: 공급자(IdP)의 이메일을 무조건 믿으면 안 되는 이유 소셜 공급자마다 '이메일 인증 여부' 를 다루는 정책이 완전히 다릅니다: 공급자 이메일 인증 여부 필드 특징 Google email_verified (boolean) 대부분 true이지만 예외적인 경우(도메인 미인증 계정 등) false 가능 Kakao kakao_account.is_email_verified 이메일 인증을 안 거친 계정도 존재할 수 있음! (is_email_valid도 별도 존재) Naver ❌ 인증 여부 필드를 아예 안 줌 네이버는 응답 객체에 이메일 인증 여부 플래그 자체가 없음 문제의 코드 (Before) // 기존 OAuth2UserInfo - 공급자가 넘겨준 email 문자열만 그대로 추출 public class OAuth2UserInfo { private String email; public static OAuth2UserInfo of(String registrationId, Map<String, Object> attributes) { // ... 공급자별 파싱 로직 // ❌ 치명적 실수: 공급자가 '이 이메일의 소유권을 인증했는지' 확인하는 플래그를 전혀 읽지 않음! return new OAuth2UserInfo(email); } } 결국 "공급자가 넘겨준 이메일은 이미 검증된 이메일일 것이다"라는 암묵적 가정 이 보안 구멍을 만든 근본 원인이었습니다. 🛠️ 3. 해결 방안: 어떻게 방어했을까? 이 취약점을 해결하기 위해 3중 방어 체계를 구축했습니다. 1 공급자별 이메일 인증 여부(emailVerified) 필수 해석 public class OAuth2UserInfo { private String email; private boolean emailVerified; // 인증 여부 필드 추가 public static OAuth2UserInfo ofKakao(Map<String, Object> attributes) { Map<String, Object> account = (Map<String, Object>) attributes.get("kakao_account"); String email = (String) account.get("email"); // 카카오: 인증 여부(is_email_verified)와 유효성(is_email_valid) 모두 확인 boolean verified = Boolean.TRUE.equals(account.get("is_email_verified")) && Boolean.TRUE.equals(account.get("is_email_valid")); return new OAuth2UserInfo(email, verified); } public static OAuth2UserInfo ofNaver(Map<String, Object> attributes) { // 네이버: 인증 여부 필드를 제공하지 않으므로 기본적으로 '미확인(false)' 처리 return new OAuth2UserInfo(email, false); } } 2 OAuth2SuccessHandler 교차 로그인 보안 규칙 강화 구글 / 카카오 (미인증 이메일) : 기존 계정이 있더라도 로그인을 단호히 거절 하고 에러 페이지로 리다이렉트 ( /login?error=unverified_email ). 사용자에게 해당 소셜 플랫폼에서 이메일 인증을 완료하고 오도록 안내. 네이버 (인증 여부 미제공) : 기존 계정의 가입 방식(Provider)이 이미 NAVER일 때만 로그인 허용 . 기존 계정이 일반 이메일 가입자라면 자동으로 엮지 않고 명시적인 계정 연동을 요구 ( /login?error=link_required ). 3 후속 아키텍처 개선: 이메일이 아닌 '소셜 고유 식별자(ID)'로 1순위 식별 이메일은 변경될 수 있고 공급자 정책에 따라 위험이 따르므로, 장기적으로는 social_accounts 매핑 테이블을 두고 소셜 고유 ID로 식별하도록 개선했습니다: Google: sub Kakao: id Naver: response.id 최초 1회만 엄격한 인증 검증 후 연동을 맺어두면, 이후에는 소셜 쪽 이메일 상태와 무관하게 안전하고 빠른 로그인이 보장됩니다. 🧪 4. 검증 결과 단위 테스트 작성 : OAuth2SuccessHandlerTest (8개 시나리오 - 미인증 이메일 거절, 교차 연동 차단 등 100% 검증) 전체 회귀 테스트 336개 ALL PASS 운영 배포 : 다운타임 없이 안전하게 배포 완료, 기존 정상 인증 회원들의 로그인에는 영향 없이 비정상 접근만 완벽 차단. 💡 이번 트러블슈팅을 통해 얻은 교훈 외부 플랫폼(IdP)의 데이터를 맹신하지 말 것 : "구글/카카오니까 당연히 검증되었겠지"라는 생각이 가장 위험합니다. 이메일을 소셜 계정의 유일한 식별자로 쓰지 말 것 : 이메일 대신 공급자의 고유 회원 식별 번호( sub , id )를 Key로 사용하는 것이 정석입니다. 보안 검증은 반드시 서버에서 강제할 것 : 프론트엔드 화면에서 아무리 가드를 쳐도, 엔드포인트 직통 호출을 막으려면 백엔드 비즈니스 로직에서 인증 여부를 직접 검증해야 합니다.
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
[보안] 소셜 로그인에서 이메일 인증 여부를 확인하지 않으면 생기는 일 (OAuth Account Takeover 방어기). "소셜 로그인은 당연히 공급자(Google, Kakao, Naver)가 이메일을 인증해 줬겠지?" 이 안일한 가정 하나가 서비스 전체의 관리자 계정 탈취(Account Takeover) 취약점으로 이어질 수 있습니다. 실제 서비스 운영 중 코드 리뷰를 진행하다가 발견한 치명적인 OAuth 계정 선점 및 탈취 취약점 과, 이를 프로덕션 무중단 배포로 안전하게 방어해 낸 과정을 공유합니다. 🚨 1. 문제의 발단: 어떻게 계정이 털릴 수 있었을까? 기존 시스템의 소셜 로그인 로직은 일반적인 웹 서비스들과 비슷하게 구성되어 있었습니다: 사용자가 카카오/구글/네이버로 로그인 요청.…
Открыть источник