로그인 후 돌아갈 주소를 쿼리로 받았다가 피싱 통로가 될 뻔한 이유
문제 발생
로그인 후 원래 보던 페이지로 돌려보내려고 쿼리 파라미터를 그대로 썼습니다.
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로 자기 주소를 넘기는 구조라, 이 값을 다루는 코드가 구조적으로 존재합니다 — 그래서 이 검증이 선택이 아닙니다.
해결 방안
- 허용 목록으로만 통과시킵니다. 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() : "/";
}-
프로토콜 상대 URL을 명시적으로 막습니다.
//evil.com은 상대 경로처럼 보이지만 브라우저는 외부 절대 URL로 해석합니다.startsWith("/")만 검사하는 코드가 여기서 뚫립니다. -
javascript:같은 스킴도 걸러집니다. 위처럼URL로 파싱하고 origin을 비교하면 스킴 검사가 함께 이루어집니다. 문자열 접두사 비교로는 부족합니다. -
더 안전한 대안은 목적지를 서버가 정하는 것입니다. OWASP가 제안하는 방식 — 사용자는 짧은 이름·ID·토큰만 제공하고 서버가 그것을 전체 URL로 매핑합니다.
?returnTo=dev-dashboard같은 형태입니다. -
로그인 성공 경로 하나로 모읍니다. 여러 곳에서 각자 리다이렉트를 만들면 한 곳만 검증을 빠뜨려도 구멍이 됩니다. MEOKKO는
@meokko/auth/return-to의resolveReturnTo()만 거치게 하고 있습니다. -
테스트로 고정합니다. 외부 origin,
//evil.com, 깨진 URL, 빈 문자열을 넣고 전부/로 폴백하는지 확인합니다 — 이 코드는 조용히 되돌아가기 쉽습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.