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/templaterather thantext/templateto render HTML. Usingtext/templateto 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.HTMLortemplate.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/templateinserts{{.Content}}without HTML-specific escaping. Input such ascontent=<script>alert('xss')</script>can become executable script in the generated page. - After:
html/templateprocesses 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.