본문으로 건너뛰기
개발 머꼬
개발 노트보안
hohyeon.dev22

로그인 후 돌아갈 주소를 쿼리로 받았다가 피싱 통로가 될 뻔한 이유

  • #Auth
  • #Engineering Note
  • #보안

문제 발생

로그인 후 원래 보던 페이지로 돌려보내려고 쿼리 파라미터를 그대로 썼습니다.

const returnTo = searchParams.get("returnTo") ?? "/";
redirect(returnTo);

그러면 이런 링크가 성립합니다.

https://meokko.com/sign-in?returnTo=https://meokko-login.example/steal

사용자에게는 주소창에 우리 도메인이 보이는 정상 로그인 링크이고, 로그인 직후 외부 사이트로 이동합니다.

원인 분석

목적지를 사용자가 정하게 뒀기 때문입니다. OWASP는 이 취약점을 이렇게 정의합니다 — 웹 애플리케이션이 신뢰할 수 없는 입력을 받아들여, 그 입력에 담긴 URL로 요청을 리다이렉트하게 될 때 발생합니다.

위험한 이유도 명확합니다 — 공격자가 리다이렉트 URL을 바꿔 피싱을 성공시키고 사용자 자격증명을 훔칠 수 있으며, 원래 도메인 이름이 그대로 보이기 때문에 거짓 신뢰가 만들어집니다. 접근 제어 우회에도 쓰일 수 있습니다.

MEOKKO는 canonical 로그인이 meokko.com에 하나만 있고 다른 서브도메인이 returnTo로 자기 주소를 넘기는 구조라, 이 값을 다루는 코드가 구조적으로 존재합니다 — 그래서 이 검증이 선택이 아닙니다.

해결 방안

  1. 허용 목록으로만 통과시킵니다. OWASP 권고 그대로 — 신뢰하는 URL 목록(호스트 목록이나 정규식)을 만들어 입력을 정제합니다. 차단 목록이 아니라 허용 목록입니다.
export function resolveReturnTo(raw: string | null, trustedOrigins: string[]): string {
  if (!raw) return "/";
  let url: URL;
  try {
    url = new URL(raw, "https://meokko.com");
  } catch {
    return "/";
  }
  return trustedOrigins.includes(url.origin) ? url.toString() : "/";
}
  1. 프로토콜 상대 URL을 명시적으로 막습니다. //evil.com은 상대 경로처럼 보이지만 브라우저는 외부 절대 URL로 해석합니다. startsWith("/")만 검사하는 코드가 여기서 뚫립니다.

  2. javascript: 같은 스킴도 걸러집니다. 위처럼 URL로 파싱하고 origin을 비교하면 스킴 검사가 함께 이루어집니다. 문자열 접두사 비교로는 부족합니다.

  3. 더 안전한 대안은 목적지를 서버가 정하는 것입니다. OWASP가 제안하는 방식 — 사용자는 짧은 이름·ID·토큰만 제공하고 서버가 그것을 전체 URL로 매핑합니다. ?returnTo=dev-dashboard 같은 형태입니다.

  4. 로그인 성공 경로 하나로 모읍니다. 여러 곳에서 각자 리다이렉트를 만들면 한 곳만 검증을 빠뜨려도 구멍이 됩니다. MEOKKO는 @meokko/auth/return-toresolveReturnTo()만 거치게 하고 있습니다.

  5. 테스트로 고정합니다. 외부 origin, //evil.com, 깨진 URL, 빈 문자열을 넣고 전부 /로 폴백하는지 확인합니다 — 이 코드는 조용히 되돌아가기 쉽습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.