클라이언트 측 검증되지 않은 URL 리다이렉트 (Open Redirect)

Open Redirect (Client-Side)

설명

클라이언트 측 Open Redirect는 사용자가 제어할 수 있는 값(쿼리스트링, 해시, DOM 값 등)을 그대로 location.assign/replace/href 등에 넣어 리다이렉트할 때 발생합니다. 검증 없이 외부 URL로 이동이 가능해지면 공격자가 조작한 링크를 통해 사용자를 신뢰된 사이트에서 악성 사이트로 보내 피싱이나 토큰 탈취를 유도할 수 있습니다. 예를 들어, 신뢰된 도메인의 링크에 to 파라미터를 악성 도메인으로 설정하면 사용자는 페이지 로드 후 자동으로 악성 사이트로 이동합니다.

잠재적 영향

  • 피싱/자격 증명 탈취: 사용자를 공격자 사이트로 유도해 로그인 정보, MFA 코드 등을 입력하게 만듭니다.
  • OAuth 토큰/코드 탈취: Open Redirect 체인을 이용해 redirect_uri 검증 우회를 시도하여 Authorization Code나 Access Token을 가로챌 수 있습니다.
  • 악성코드 배포 및 브랜드 훼손: 신뢰된 도메인을 경유해 악성 다운로드로 이동시키고, 서비스 신뢰도를 떨어뜨립니다.
  • 보안 정책 우회: 리퍼러 기반 필터링이나 일부 네비게이션 보호 논리를 우회해 외부로 이동시킬 수 있습니다.

해결 방법

  • 사용자 입력을 리다이렉트에 직접 사용하지 마세요.
  • 허용 목록(allowlist) 기반 매핑: to=home 같은 키를 내부 경로로 매핑하고 목록 외 값은 기본 경로로 처리하세요.
  • 내부 경로만 허용: 스킴(http, https)이나 호스트가 있는 절대 URL, 프로토콜 상대 URL(//evil.com)은 차단하세요.
  • URL 검증: new URL(value, location.origin)으로 파싱 후 origin이 현재 사이트와 동일한지, pathname이 '/'로 시작하는지 확인하세요.
  • 서버와 클라이언트 모두에서 검증: 가능하면 서버에서 최종 리다이렉트 수행 시에도 동일한 allowlist 검사를 적용하세요.

예시

변경 전

javascript
// 변경 전: 해시(fragment)의 값을 그대로 사용해 외부로 리다이렉트
function unsafeRedirect() {
  // URL: https://trusted.example/app#to=https://evil.example
  const raw = location.hash.replace(/^#to=/, '');
  if (raw) {
    // 사용자 제어 값이 절대 URL이면 외부로 이탈
    top.location = decodeURIComponent(raw);
  }
}

변경 후

javascript
// 변경 후: allowlist 키를 내부 경로로만 매핑하여 리다이렉트
const ROUTE_MAP = Object.freeze({
  home: '/home',
  dashboard: '/dashboard',
  help: '/help'
});

function safeRedirect() {
  const params = new URLSearchParams(window.location.search);
  const key = params.get('to');
  const path = Object.hasOwn(ROUTE_MAP, key) ? ROUTE_MAP[key] : undefined;

  if (path) {
    // 내부 경로로만 이동 (스킴/호스트 불가)
    window.location.assign(path);
  } else {
    // 허용되지 않은 값은 기본 경로로 처리
    window.location.replace('/home');
  }
}

설명:

  • 변경 전: 사용자 제어 값(raw)을 그대로 location에 대입하여 절대 URL(https://..., //evil.com 등)도 허용합니다. 공격자는 신뢰된 도메인의 링크에 #to=외부URL을 붙여 사용자를 악성 사이트로 자동 이동시켜 피싱과 토큰 탈취를 유도할 수 있습니다.
  • 변경 후: 사용자 입력을 직접 사용하지 않고, 객체 자체에 정의한 allowlist 키만 확인해 내부 경로로 매핑합니다. toString 같은 상속 속성은 선택하지 않습니다. 외부 도메인이나 절대 URL은 선택지에 없으므로 리다이렉트가 내부로만 제한되어 Open Redirect 위험이 제거됩니다.

참조