오픈 리다이렉트(Open Redirect)

Open Redirect

설명

오픈 리다이렉트(Open Redirect)는 HTTP 리다이렉트용 URL이 사용자 입력값으로 그대로 만들어질 때 발생하는 취약점입니다. 공격자는 리다이렉트 대상 URL을 조작해 사용자를 악성 사이트(피싱 사이트, 멀웨어 배포 사이트 등)로 보내도록 만들 수 있습니다. 예를 들어, 공격자가 링크에 특정 파라미터나 Host, URL Path를 넣어 서버가 그대로 http.Redirect에 사용하면, 사용자는 정상 사이트를 방문한다고 생각하지만 실제로는 공격자가 지정한 외부 사이트로 이동하게 됩니다.

잠재적 영향

  • 피싱 및 사회공학 공격(피싱 유도): 사용자가 신뢰하는 도메인을 거쳐 악성 사이트로 자동 이동되기 때문에 피싱 공격의 신뢰도가 크게 높아집니다.
  • 악성 코드 유포(악성 사이트로 리다이렉트): 악성 코드 배포 사이트나 익스플로잇 페이지로 사용자를 보내 브라우저 취약점 악용, 랜섬웨어 감염 등을 유도할 수 있습니다.
  • 브랜드/서비스 신뢰도 하락(서비스 신뢰도 손상): 공격에 악용되면 사용자는 원래 서비스가 보안에 취약하다고 인식하여 브랜드 이미지와 사용자 신뢰가 떨어질 수 있습니다.

해결 방법

  • 리다이렉트 대상 URL 검증: http.Redirect에 넘기기 전에 반드시 검증하세요. 특히 scheme(http, https), host, port가 허용된 값인지 확인하세요.
  • 도메인 allowlist 사용: 외부 도메인으로 리다이렉트가 필요하다면, 허용된 도메인 목록(allowlist)에 있는지 검사한 후에만 리다이렉트하세요.
  • 상대 경로만 허용: 가능하면 /path/... 형태의 상대 경로만 허용하고, 절대 URL(http://, https://)과 //host 형태는 거부하세요.
  • Host 헤더 신뢰 금지: req.Host는 클라이언트가 임의로 조작할 수 있으므로 그대로 리다이렉트 URL에 사용하지 말고, 서버 측에서 설정한 고정 host를 사용하세요.
  • URL 정규화 및 검증: url.Parse, ResolveReference 등을 사용해 URL을 정규화(normalize)한 뒤, 최종 host와 scheme이 기대한 값인지 검사하세요.

예시

변경 전

go
package main

import (
    "net/http"
)

// 취약한 예: 사용자 입력(Host, URL Path)을 그대로 사용
func redirectHandler(w http.ResponseWriter, r *http.Request) {
    // req.Host 와 r.URL.Path 는 클라이언트가 조작할 수 있는 값
    target := "https://" + r.Host + r.URL.Path
    if len(r.URL.RawQuery) > 0 {
        target += "?" + r.URL.RawQuery
    }

    // 사용자 입력으로 만들어진 URL로 직접 리다이렉트 → Open Redirect 취약점
    http.Redirect(w, r, target, http.StatusTemporaryRedirect)
}

func main() {
    http.HandleFunc("/go", redirectHandler)
    http.ListenAndServe(":8080", nil)
}

변경 후

go
package main

import (
    "net/http"
    "net/url"
    "strings"
)

var allowedHosts = map[string]bool{
    "example.com":      true,
    "www.example.com":  true,
}

// 안전한 예: 고정 도메인 사용 + 상대 경로만 허용
func safeRedirectHandler(w http.ResponseWriter, r *http.Request) {
    // 1) 리다이렉트는 항상 서버에서 정한 고정 호스트로
    const baseURL = "https://www.example.com"

    // 2) path 파라미터를 통해 이동할 경로를 전달받는다고 가정
    //    예) /go?next=/products/123
    next := r.URL.Query().Get("next")
    if next == "" {
        next = "/" // 기본 경로
    }

    // 3) 절대 URL(예: http://evil.com) 이나 프로토콜-relative(//evil.com) 은 차단
    if strings.HasPrefix(next, "http://") ||
        strings.HasPrefix(next, "https://") ||
        strings.HasPrefix(next, "//") {
        http.Error(w, "invalid redirect url", http.StatusBadRequest)
        return
    }

    // 4) 항상 '/' 로 시작하는 상대 경로만 허용
    if !strings.HasPrefix(next, "/") {
        next = "/" + next
    }

    // 최종 리다이렉트 URL 조합
    u, err := url.Parse(baseURL)
    if err != nil {
        http.Error(w, "server error", http.StatusInternalServerError)
        return
    }

    u.Path = next

    // 5) 최종 host 가 allowlist 에 있는지 검증 (예방적 예시)
    if !allowedHosts[u.Host] {
        http.Error(w, "invalid redirect host", http.StatusBadRequest)
        return
    }

    http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
}

func main() {
    http.HandleFunc("/go", safeRedirectHandler)
    http.ListenAndServe(":8080", nil)
}

설명:

  • 변경 전: 취약한 코드에서는 r.Host, r.URL.Path, r.URL.RawQuery를 그대로 이어 붙여 리다이렉트 URL을 만들고 있습니다. 이 값들은 모두 클라이언트가 조작할 수 있습니다.
  • 이 예시에서는 공격자가 Host 헤더를 evil.com으로 바꾸면 외부 호스트가 리다이렉트 대상으로 사용됩니다.
  • 서버는 이 값을 검증하지 않고 http.Redirect에 그대로 사용하기 때문에 사용자가 공격자가 지정한 외부 악성 사이트로 이동하게 됩니다.
  • 결과적으로, 애플리케이션 도메인을 신뢰하는 사용자가 손쉽게 피싱/악성 사이트로 유도될 수 있습니다.
  • 변경 후: 안전한 코드에서는 다음과 같은 방식으로 취약점을 제거합니다.
  • 리다이렉트 기준 도메인을 서버 코드에서 고정(baseURL)하여 r.Host를 신뢰하지 않습니다.
  • 사용자가 지정할 수 있는 값은 경로(next)만으로 제한하고, 항상 /로 시작하는 상대 경로만 허용합니다.
  • http://, https://, //로 시작하는 입력을 거부하고, 허용된 입력도 url.URL.Path에 넣어 고정 호스트의 경로로 처리합니다.
  • 최종적으로 조합된 URL의 host가 allowlist에 포함되어 있는지 검증해, 의도치 않은 호스트로 리다이렉트되는 것을 한 번 더 방지합니다. 이렇게 함으로써 사용자가 조작할 수 있는 부분을 최소화하고, 외부 악성 사이트로의 오픈 리다이렉트를 예방할 수 있습니다.

참조