Cross-site scripting (XSS)

Cross-site scripting

Description

template.HTMLAttr treats a string as a trusted HTML attribute without escaping it. If you construct an entire attribute from user input and convert it to this type, an attacker can use quotes to break out of the value and inject an event handler or another attribute.

Potential impact

  • An attacker may execute JavaScript in the victim's page.
  • This may expose data accessible to the page, alter its content, or send requests with the user's privileges.

Remediation

  • Keep attribute names and structure fixed in the template, and pass values as ordinary strings.
  • Use the context-aware escaping in html/template instead of converting user input to template.HTMLAttr.
  • Restrict characters and length according to the value's intended use.

Examples

Before

go
package main

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

func unsafeHandler(w http.ResponseWriter, r *http.Request) {
    // Read user input from the query parameter
    color := r.URL.Query().Get("color") // Expected input, e.g. "red"

    // Unsafe: concatenate user input into an HTML attribute
    // Attack example: color="red" onclick="alert(1)"
    attr := "style=\"color:" + color + "\""

    // template.HTMLAttr does not escape the value
    safeAttr := template.HTMLAttr(attr)

    // Use attr directly in the template
    t := template.Must(template.New("page").Parse(`
        <html><body>
            <p {{.}}>Hello</p>
        </body></html>`))

    // Injected attributes may execute code in the browser
    _ = t.Execute(w, safeAttr)
}

After

go
package main

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

var pageTmpl = template.Must(template.New("page").Parse(`
<html><body>
    <p style="color:{{.Color}}">Hello</p>
</body></html>`))

// Limit characters and length; this does not validate CSS color names
var colorRe = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,20}$`)

func safeHandler(w http.ResponseWriter, r *http.Request) {
    color := r.URL.Query().Get("color")
    if !colorRe.MatchString(color) {
        // Use the default when validation fails
        color = "black"
    }

    // Pass an ordinary string to html/template
    // The template escapes it for the output context
    data := struct {
        Color string
    }{Color: color}

    if err := pageTmpl.Execute(w, data); err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
}

Explanation: Before, input such as red" onclick="alert(1) can create a new event attribute. After, Color is passed as an ordinary string and receives filtering and escaping appropriate for a CSS value. The regular expression limits characters and length; it does not guarantee that every accepted value is a valid CSS color.

References