설명
오픈 리다이렉트는 애플리케이션이 사용자가 보낸 URL을 검증하지 않고 redirect, sendRedirect 같은 이동 응답에 사용할 때 발생합니다. 사용자는 정상 도메인으로 시작하는 링크를 보게 되지만, 실제로는 외부 피싱 사이트나 악성 다운로드 페이지로 이동할 수 있습니다.
로그인 후 이동 경로, OAuth 콜백 후 returnUrl, 결제 완료 후 이동 페이지처럼 리다이렉트가 자연스러운 기능에서 자주 생깁니다.
잠재적 영향
- 피싱 위험: 신뢰할 수 있는 서비스 주소를 이용해 사용자를 가짜 로그인 페이지로 보낼 수 있습니다.
- 인증 흐름 악용: OAuth나 SSO 흐름에서 검증되지 않은 리다이렉트가 토큰 노출이나 세션 탈취로 이어질 수 있습니다.
- 악성 콘텐츠 유도: 사용자를 악성 파일 다운로드 페이지나 취약한 외부 사이트로 보낼 수 있습니다.
- 보안 정책 우회: 내부에서 허용된 도메인을 거쳐 외부 URL로 이동하게 만들어 필터링을 우회할 수 있습니다.
해결 방법
- 리다이렉트 대상은 서버가 관리하는 허용 목록의 경로나 도메인으로 제한합니다.
- 가능하면 URL 전체를 입력받지 말고,
next=profile처럼 고정된 식별자를 받아 서버에서 실제 경로로 매핑합니다. - 외부 URL이 꼭 필요하다면 스킴, 호스트, 포트, 정규화 결과를 모두 확인하고
https만 허용합니다. startsWith,contains같은 문자열 검사만으로 신뢰하지 않습니다.https://trusted.example.com.evil.example처럼 우회될 수 있습니다.- 허용되지 않은 값은 기본 안전 경로로 보내거나 요청을 거부합니다.
예시
Spring 컨트롤러의 일부이며, 필요한 import와 애플리케이션의 내부 경로 정의는 생략했습니다.
변경 전
java
@GetMapping("/login/success")
public void loginSuccess(
@RequestParam("next") String next,
HttpServletResponse response
) throws IOException {
response.sendRedirect(next);
}
변경 후
java
private static final Map<String, String> ALLOWED_REDIRECTS = Map.of(
"profile", "/account/profile",
"orders", "/account/orders"
);
@GetMapping("/login/success")
public void loginSuccess(
@RequestParam(value = "next", defaultValue = "profile") String next,
HttpServletResponse response
) throws IOException {
String redirectPath = ALLOWED_REDIRECTS.getOrDefault(next, "/account/profile");
response.sendRedirect(redirectPath);
}
설명:
- 변경 전: 요청 파라미터의 URL을 그대로 리다이렉트에 사용합니다. 사용자가 정상 서비스 링크를 클릭했더라도 외부 사이트로 이동할 수 있습니다.
- 변경 후: 사용자가 보낸 값은 실제 URL이 아니라 서버가 아는 식별자입니다. 허용 목록에 없는 값은 기본 안전 경로로 처리합니다.