설명
template.URL은 값을 신뢰할 수 있는 URL로 표시하는 타입입니다. 입력을 검증하지 않으며, html/template에서 사용하면 일반 문자열에 적용되는 URL 스킴 필터를 우회할 수 있습니다. 사용자 입력이 포함된 링크는 일반 문자열로 템플릿에 전달하고, 허용할 목적지도 검증하세요.
잠재적 영향
- 위험한 URL이나 이스케이프되지 않은 HTML 속성으로 인해 페이지에서 스크립트가 실행될 수 있습니다.
- 공격자는 페이지에 접근 가능한 정보를 읽거나 사용자 권한으로 요청을 보낼 수 있습니다.
해결 방법
- 사용자 입력을
template.URL로 변환하지 마세요. - HTML을 직접 이어 붙이지 말고
html/template의 맥락별 이스케이프를 사용하세요. - 허용할 목적지를 명시적으로 제한하고, 쿼리 값은
url.QueryEscape로 인코딩하세요. 쿼리 인코딩은 HTML 이스케이프나 목적지 검증을 대신하지 않습니다.
예시
변경 전
go
package main
import (
"html/template"
"net/http"
)
// 비준수 예제: 사용자 입력을 그대로 template.URL()에 전달
func UserProfileHandler(w http.ResponseWriter, r *http.Request) {
// 사용자 입력 (예: /profile?redirect=https://example.com)
redirect := r.URL.Query().Get("redirect")
// 공격자가 작은따옴표를 포함한 입력으로 href 속성을 벗어날 수 있음
// ✗ 취약: 사용자 입력이 포함된 문자열을 그대로 template.URL()에 넘김
url := "/go?next=" + redirect
// template.URL로 변환해도 직접 작성하는 HTML은 이스케이프되지 않음
safeURL := template.URL(url)
// HTML 출력 템플릿을 문자열로 직접 작성
html := "<a href='" + string(safeURL) + "'>계속하기</a>"
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
변경 후
go
package main
import (
"html/template"
"net/http"
"net/url"
)
var tmpl = template.Must(template.New("link").Parse(`
<!DOCTYPE html>
<html>
<body>
<a href="{{.SafeNext}}">계속하기</a>
</body>
</html>
`))
// 준수 예제: 사용자 입력 검증 + escaping + html/template 사용
func UserProfileHandler(w http.ResponseWriter, r *http.Request) {
rawNext := r.URL.Query().Get("redirect")
// 1. 명시적으로 허용한 내부 목적지만 사용
if rawNext != "/home" && rawNext != "/profile" {
rawNext = "/home"
}
// 2. 목적지를 next 쿼리 파라미터의 값으로 인코딩
encodedNext := url.QueryEscape(rawNext)
// 3. 템플릿에 데이터로 전달 (html/template가 자동으로 escape 처리)
data := struct {
SafeNext string
}{
SafeNext: "/go?next=" + encodedNext,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
}
설명: 변경 전에는 입력의 작은따옴표가 href 속성을 벗어나 HTML을 삽입할 수 있습니다. 이 예시에서 javascript:를 /go?next= 뒤에 붙이는 것만으로 실행 가능한 스킴이 되지는 않습니다. 변경 후에는 /home과 /profile만 허용하고, 목적지를 쿼리 값으로 인코딩한 링크를 템플릿에 전달합니다. 실제 리디렉션을 수행하는 /go 처리기에서도 목적지를 검증해야 합니다.