text/template vs html/template: how does contextual auto-escaping prevent XSS, and what does template.HTML bypass?
The two packages share the same syntax. text/template does no escaping. html/template parses the HTML around each action and works out its context: element body, attribute, URL attribute, <script> (JS), CSS, or JS string. It then applies the right escaper for that context. So the same value is HTML-entity-escaped in the body, JS-escaped inside onclick, and checked in href, where unsafe schemes such as javascript: are replaced by #ZgotmplZ.
const page = `<a href="{{.URL}}" onclick="greet({{.Name}})">{{.Name}}</a>`
t := template.Must(template.New("p").Parse(page)) // html/template
t.Execute(os.Stdout, map[string]any{
"URL": "javascript:alert(1)",
"Name": `<script>alert("x")</script>`,
})
// href="#ZgotmplZ"
// onclick="greet("")" (quoted JS string literal)
// body: <script>alert("x")</script>
Typed strings bypass escaping. template.HTML, template.JS, template.URL, template.CSS, template.HTMLAttr and template.Srcset tell the engine "this content is already trusted", so it is inserted as-is. Wrapping user input in template.HTML(userInput) puts the XSS right back. Only use these types on constants or on output from a real sanitizer such as bluemonday.
Gotchas: importing text/template by accident, for example through goimports, compiles fine but is vulnerable. Escaping happens when the template is parsed and first executed. Templates cannot detect contexts they cannot parse, such as values built into a <script> tag name at runtime. And html/template does not protect you from server-side injection if you let users write the template text itself.
More on More Standard Library Essentials
- Q534How does Go's time layout work (the reference time Mon Jan 2 15:04:05 MST 2006)? What bugs come from using "2006-01-02" vs "YYYY-MM-DD", time zones and time.Parse vs time.ParseInLocation?
- Q535Compare math/rand, math/rand/v2 and crypto/rand. What changed about seeding in Go 1.20 and 1.22, and when must you use crypto/rand?
- Q537How do you prevent SQL injection with database/sql? Why can't placeholders be used for table names or ORDER BY columns?
- Q538Why should you compare secrets with crypto/subtle.ConstantTimeCompare instead of ==? How do you hash passwords in Go?
- Q539How does regexp (RE2) differ from PCRE engines? Why doesn't Go support backreferences, and what are the performance guarantees?
- Q540How do you run external commands safely with os/exec? Explain CommandContext, capturing stdout/stderr, exit codes, and why shell injection is not the default risk.