Cross-site scripting (XSS)

Cross-site scripting (XSS)

Description

Rendering user input in HTML without appropriate escaping may let attackers inject <script> tags or event handlers such as onclick. If a browser interprets the input as active page content, the attacker's JavaScript can execute in the victim's browser, enabling session abuse, phishing or keylogging.

Potential impact

  • Attackers may steal script-readable cookies or tokens, or make requests with the user's permissions.
  • Malicious JavaScript can alter the DOM, redirect the browser or record keystrokes.
  • Forged login or payment forms may trick users into disclosing sensitive data.
  • Malicious popups and redirects can damage trust and cause financial or legal consequences.

Remediation

  • For HTML responses, use trusted templates with html/template and pass user input as ordinary data.
  • http.ResponseWriter.Write() is not inherently unsafe. Do not serve HTML assembled directly from user input.
  • Handle values for their HTML, URL or JavaScript context, and never build template source from user input.
  • Do not cast untrusted input to trust-marking types such as template.HTML, template.URL or template.JS.
  • Validate URL and name formats, but do not treat input validation as a replacement for output-context handling.

Examples

These examples return HTML. URL-context handling in html/template restricts dangerous schemes and attribute injection; it does not define a policy for allowed external-link domains.

Before

go
package main

import (
    "fmt"
    "net/http"
)

// Before: write user input directly through ResponseWriter
func errorPage(w http.ResponseWriter, r *http.Request) {
    // A query parameter controlled by the user
    url := r.URL.Query().Get("url")

    // HTML containing user input
    const template = `
    <html>
    <body>
      <h1>error; page not found. <a href="%s">go back</a></h1>
    </body>
    </html>`

    // Format the string, then write it directly
    // The url may contain "javascript:alert(1)" or "\" onclick=\"alert(1)"
    w.WriteHeader(http.StatusBadRequest)
    w.Write([]byte(fmt.Sprintf(template, url)))
}

func main() {
    http.HandleFunc("/error", errorPage)
    http.ListenAndServe(":8080", nil)
}

After

go
package main

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

// After: automatic escaping with html/template

// The template receives data at {{.URL}},
// and html/template applies context-aware escaping
var errorTmpl = template.Must(template.New("error").Parse(`
<html>
<body>
  <h1>error; page not found. <a href="{{.URL}}">go back</a></h1>
</body>
</html>`))

func errorPageSafe(w http.ResponseWriter, r *http.Request) {
    url := r.URL.Query().Get("url")

    // Use a default when the URL is empty
    if url == "" {
        url = "/"
    }

    w.WriteHeader(http.StatusBadRequest)

    // Pass the data structure to the template
    data := struct {
        URL string
    }{URL: url}

    if err := errorTmpl.Execute(w, data); err != nil {
        log.Println("template execute error:", err)
    }
}

func main() {
    http.HandleFunc("/error", errorPageSafe)
    http.ListenAndServe(":8080", nil)
}

Explanation:

  • Before: fmt.Sprintf() inserts the url parameter directly into HTML, then w.Write() sends it to the browser. Input such as " onclick="alert(1) can break out of the <a> attribute and introduce JavaScript.
  • After: An ordinary string is passed to the trusted template's href position. html/template handles that URL context, restricting dangerous schemes and attribute breakout. Do not bypass this handling by casting user input to template.URL or template.HTML.

References