Cross-site scripting (XSS)

Cross-site scripting (XSS)

Description

template.URL marks a value as a trusted URL. It does not validate input, and using it in html/template can bypass the URL-scheme filter applied to ordinary strings. Pass links containing user input to the template as ordinary strings and validate their destinations.

Potential impact

  • A dangerous URL or an unescaped HTML attribute may allow scripts to run in the page.
  • An attacker may read data accessible to the page or send requests with the user's privileges.

Remediation

  • Do not convert user input to template.URL.
  • Use the context-aware escaping in html/template instead of concatenating HTML.
  • Explicitly restrict allowed destinations and encode query values with url.QueryEscape. Query encoding does not replace HTML escaping or destination validation.

Examples

Before

go
package main

import (
    "html/template"
    "net/http"
)

// Unsafe: pass user input directly to template.URL()
func UserProfileHandler(w http.ResponseWriter, r *http.Request) {
    // User input, e.g. /profile?redirect=https://example.com
    redirect := r.URL.Query().Get("redirect")

    // Input containing a single quote can break out of the href attribute

    // Vulnerable: pass a string containing user input to template.URL()
    url := "/go?next=" + redirect

    // Converting to template.URL does not escape manually constructed HTML
    safeURL := template.URL(url)

    // Construct the HTML output by concatenating strings
    html := "<a href='" + string(safeURL) + "'>계속하기</a>"

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write([]byte(html))
}

After

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>
`))

// Validate input and use escaping with html/template
func UserProfileHandler(w http.ResponseWriter, r *http.Request) {
    rawNext := r.URL.Query().Get("redirect")

    // 1. Use only explicitly allowed internal destinations
    if rawNext != "/home" && rawNext != "/profile" {
        rawNext = "/home"
    }

    // 2. Encode the destination as the next query parameter value
    encodedNext := url.QueryEscape(rawNext)

    // 3. Pass data to the template for automatic escaping
    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
    }
}

Explanation: Before, a single quote in the input can break out of the href attribute and inject HTML. Appending javascript: after /go?next= does not itself create an executable URL scheme in this example. After, only /home and /profile are allowed, and the link passed to the template contains the encoded destination as a query value. The /go handler that performs the redirect must also validate the destination.

References