크로스 사이트 스크립팅 (XSS)

Cross-Site Scripting (XSS)

설명

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 처리기에서도 목적지를 검증해야 합니다.

참조