Automatic Escaping and XSS
html/template escapes each value for the exact place it lands in, which is what stops a comment field becoming a script.
type HTML string // template.HTML(s) marks s as trusted and skips escapingSuppose a visitor types <script>...</script> into a comment box and your page prints it back as it arrived. The browser cannot tell that text from your own markup, so it runs it. That is cross-site scripting, written XSS: data from an attacker being treated as part of your page. The defence is escaping, replacing characters that have meaning in HTML with harmless spellings, so < is sent as < and displays as a less-than sign.
Escaping depends on position
html/template escapes for you, and it escapes according to where the value lands. While parsing, it works out the context of every action and inserts the right escaper. In ordinary text it escapes HTML characters. Inside a quoted attribute it escapes attribute characters. Inside an href it escapes and filters the value as a URL. Inside a script element it escapes as JavaScript, and inside style as CSS. The same {{.}} therefore produces different output in different places, which is correct and is the reason the package is safe.
text/template has none of this. Same syntax, no escaping, every value inserted exactly as given. Use it for email bodies, config files and generated code, never for HTML.
Marking trusted content
Sometimes you have markup you do want inserted, for example output from a sanitiser. The string types template.HTML, template.JS, template.CSS, template.URL, template.Srcset and template.JSStr are inserted verbatim. The documentation is blunt: the content must come from a trusted source. One template.HTML(userInput) cancels the protection for that value.
Escaping also does nothing for HTML you assemble yourself with fmt.Fprintf and write to the response.
Example
package main
import (
"html/template"
"log"
"os"
texttemplate "text/template"
)
// One template source, executed by both packages.
const src = `<p>{{.}}</p>
`
func main() {
comment := "<b>bold</b> & more"
// text/template inserts the value exactly as given. This is the XSS.
plain := texttemplate.Must(texttemplate.New("t").Parse(src))
if err := plain.Execute(os.Stdout, comment); err != nil {
log.Fatal(err)
}
// html/template escapes it for the HTML text it lands in.
safe := template.Must(template.New("t").Parse(src))
if err := safe.Execute(os.Stdout, comment); err != nil {
log.Fatal(err)
}
}
// Output:
// <p><b>bold</b> & more</p>
// <p><b>bold</b> & more</p>
When to use it
- Show a visitor's comment on a page without letting them inject a script tag.
- Insert markup from a sanitiser without it being escaped a second time.
- Choose text/template for an email body and html/template for a page.
- Explain in review why building HTML with fmt.Sprintf is not acceptable.
More examples
URL filtering in an href
Reach for this when a user supplies a link: a profile website field, a redirect target, an avatar src. An ordinary path passes through untouched. A value whose scheme the package does not consider safe is replaced with #ZgotmplZ rather than emitted, so a javascript: payload cannot become a clickable script. Seeing ZgotmplZ in a page means a value reached a URL slot that should not have.
package main
import (
"html/template"
"log"
"os"
)
// The action sits where a URL belongs, so the value is checked as a URL.
const linkHTML = `<a href="{{.}}">link</a>
`
func main() {
t := template.Must(template.New("link").Parse(linkHTML))
for _, u := range []string{"/docs/go", "javascript:alert(1)"} {
if err := t.Execute(os.Stdout, u); err != nil {
log.Fatal(err)
}
}
}
// Output:
// <a href="/docs/go">link</a>
// <a href="#ZgotmplZ">link</a>
Opting out for markup you produced
The pattern for rendered Markdown or sanitised rich text: convert the field's type at the boundary where you trust it, and keep it as a plain string everywhere else. Putting template.HTML in the struct type, rather than converting inside the handler, makes the decision visible in one place and reviewable. Never convert a value that came straight from a request.
package main
import (
"html/template"
"log"
"os"
)
const bodyHTML = `<div>{{.Rendered}}</div>
<div>{{.Raw}}</div>
`
type post struct {
// Rendered holds markup this program produced, so it goes out verbatim.
Rendered template.HTML
// Raw holds whatever the user typed. A plain string, so it is escaped.
Raw string
}
func main() {
t := template.Must(template.New("post").Parse(bodyHTML))
p := post{
Rendered: template.HTML("<em>from the sanitiser</em>"),
Raw: "<em>from the user</em>",
}
if err := t.Execute(os.Stdout, p); err != nil {
log.Fatal(err)
}
}
// Output:
// <div><em>from the sanitiser</em></div>
// <div><em>from the user</em></div>
Hand-built HTML has no protection
A reminder that the safety belongs to the package, not to the project. One handler that writes a fragment with Fprintf reintroduces XSS in an application where every other page uses templates, and it will not look wrong in a diff. HTMLEscapeString covers the rare fragment that genuinely has no template, but the better fix is usually to give it one.
package main
import (
"fmt"
"html/template"
"log"
"os"
)
func main() {
comment := "<b>bold</b>"
// Assembling HTML with Fprintf. The value is inserted raw.
if _, err := fmt.Fprintf(os.Stdout, "<p>%s</p>\n", comment); err != nil {
log.Fatal(err)
}
// The same value through a template, escaped for its context.
t := template.Must(template.New("p").Parse("<p>{{.}}</p>\n"))
if err := t.Execute(os.Stdout, comment); err != nil {
log.Fatal(err)
}
// If you really are writing one fragment by hand, escape it yourself.
esc := template.HTMLEscapeString(comment)
if _, err := fmt.Fprintf(os.Stdout, "<p>%s</p>\n", esc); err != nil {
log.Fatal(err)
}
}
// Output:
// <p><b>bold</b></p>
// <p><b>bold</b></p>
// <p><b>bold</b></p>
Discussion