Go

text/template vs html/template: how does contextual auto-escaping prevent XSS, and what does template.HTML bypass?

Question 536HardGo 1.22 to 1.25

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(&#34;&#34;)"  (quoted JS string literal)
// body: &lt;script&gt;alert(&#34;x&#34;)&lt;/script&gt;

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

All 16 More Standard Library Essentials questions