설명
오픈 리다이렉트(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에 포함되어 있는지 검증해, 의도치 않은 호스트로 리다이렉트되는 것을 한 번 더 방지합니다. 이렇게 함으로써 사용자가 조작할 수 있는 부분을 최소화하고, 외부 악성 사이트로의 오픈 리다이렉트를 예방할 수 있습니다.