Cross-site scripting (XSS)

Cross-site scripting (XSS)

Description

Go's text/template does not automatically escape HTML. Rendering user input or external data such as titles, comments or profiles directly into an HTML document can therefore execute embedded scripts such as <script>...</script> in another user's browser. Attackers may use stored data or request parameters for session abuse, page manipulation or phishing.

Potential impact

  • Attackers may steal script-readable cookies or tokens, or send requests with the user's permissions.
  • JavaScript may record keystrokes, call APIs or access browser data.
  • Modified pages may show fake login or payment forms to collect sensitive information.
  • DOM changes can falsify displayed data or trigger additional requests.

Remediation

  • Use html/template rather than text/template to render HTML. Using text/template to produce plain text is not inherently unsafe.
  • Keep template source trusted and fixed, and pass external values as ordinary strings for context-aware escaping.
  • Do not cast user input to trusted types such as template.HTML or template.JS.
  • Use an appropriate CSP as an additional restriction on script execution.
  • Validate input formats, but do not replace output encoding with a filter for suspicious strings.

Examples

Before

go
package main

import (
    "html"
    "log"
    "net/http"
    "text/template" // No automatic HTML escaping
)

// Simple post data structure
type Post struct {
    Title   string
    Content string // User-supplied content
}

// text/template does not escape HTML
var tmpl = template.Must(template.New("post").Parse(`
<!DOCTYPE html>
<html>
  <head><title>{{.Title}}</title></head>
  <body>
    <h1>{{.Title}}</h1>
    <div>
      내용: {{.Content}}
    </div>
  </body>
</html>
`))

func handler(w http.ResponseWriter, r *http.Request) {
    // Example: receive a title and content from query parameters
    title := r.URL.Query().Get("title")
    content := r.URL.Query().Get("content")

    p := Post{Title: title, Content: content}

    if err := tmpl.Execute(w, p); err != nil {
        log.Println(err)
        http.Error(w, html.EscapeString("template error"), http.StatusInternalServerError)
        return
    }
}

func main() {
    http.HandleFunc("/post", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

After

go
package main

import (
    "html"
    "html/template" // Automatic context-aware HTML escaping
    "log"
    "net/http"
)

// Simple post data structure
type Post struct {
    Title   string
    Content string // User-supplied content
}

// html/template applies automatic escaping
var tmpl = template.Must(template.New("post").Parse(`
<!DOCTYPE html>
<html>
  <head><title>{{.Title}}</title></head>
  <body>
    <h1>{{.Title}}</h1>
    <div>
      내용: {{.Content}}
    </div>
  </body>
</html>
`))

func handler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Query().Get("title")
    content := r.URL.Query().Get("content")

    p := Post{Title: title, Content: content}

    if err := tmpl.Execute(w, p); err != nil {
        log.Println(err)
        http.Error(w, html.EscapeString("template error"), http.StatusInternalServerError)
        return
    }
}

func main() {
    http.HandleFunc("/post", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Explanation:

  • Before: text/template inserts {{.Content}} without HTML-specific escaping. Input such as content=<script>alert('xss')</script> can become executable script in the generated page.
  • After: html/template processes ordinary strings according to the trusted template's context. Here, {{.Content}} is in a text position, so input such as <script> is displayed as text rather than interpreted as a tag. Do not cast external input to a trusted HTML type.

References