Writing user input into an HTML response with fmt.Fprintf, string concatenation or the text/template package skips output encoding and creates cross-site scripting (XSS). Go's html/template package escapes values by context and is documented for exactly this job. The remaining risk is bypassing it with types like template.HTML.
What does the risky pattern look like?
The tempting version is short and works in a demo:
func hello(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
fmt.Fprintf(w, "<h1>Hello, %s</h1>", name)
}
Nothing here treats the value as data. The query parameter is pasted into markup, so whatever a visitor puts in the URL becomes part of the page structure. The code is simple, which is why it survives review: there is nothing to see.
What does html/template do instead?
The standard library documentation says html/template implements data-driven templates for generating HTML output safe against code injection. It has the same interface as text/template and should be used instead of it whenever the output is HTML. Values are treated as plain text to be encoded, and the escaping is contextual: it understands HTML, CSS, JavaScript and URI contexts and applies the right encoding for where each value lands.
var page = template.Must(template.New("hello").Parse(`<h1>Hello, {{.}}</h1>`))
func hello(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
page.Execute(w, r.URL.Query().Get("name"))
}
The import path is the whole difference between the two packages. The Go documentation shows the same template run through both: text/template emits the script tag as written, and html/template emits escaped text.
Why does the context matter?
A value placed in an HTML body needs different handling from one in an attribute, a script block or a URL. OWASP's XSS prevention cheat sheet organizes output encoding the same way, with separate guidance for HTML, attribute, JavaScript, CSS and URL contexts. Hand-written escaping usually gets one context right and the rest wrong. The Go package does the tracking for you. If a value would reach a CSS or URL context unsafely, it substitutes a harmless marker, which is the ZgotmplZ you may have seen in output.
What is the security model?
The package assumes template authors are trusted and the data passed to Execute is not. That has a consequence: the template text must never be built from user input. Only the data should be.
Where do people still get hurt?
Three bypasses show up in review.
First, template.HTML(value) and its siblings for attributes, JavaScript and URLs. The documentation states that these types present a security risk: the content is included verbatim in the output and should come from a trusted source. Converting a request value to template.HTML undoes the protection.
Second, parsing templates with text/template for convenience, for example in a helper shared with email rendering.
Third, calling the html or urlquery escapers by hand inside a pipeline. The package already escapes contextually, and the docs say manual escaping is unnecessary and may break correctness.
A constructed example: a team adds a "rich bio" feature. Someone wraps the stored bio in template.HTML so line breaks render. Every profile page now outputs whatever any user saved, to every visitor. If rich text is the requirement, sanitize with a maintained HTML sanitizer first. The Go documentation notes the output of a sound sanitizer is fine for use with the HTML type.
How to check or apply this
- Search the codebase for fmt.Fprintf, fmt.Fprint and io.WriteString calls whose format string contains angle brackets and writes to a ResponseWriter.
- Search for imports of text/template in packages that serve web pages.
- Search for template.HTML, template.JS, template.URL and template.HTMLAttr, and justify each use in a comment with the trust source.
- Set the Content-Type header explicitly on every handler that returns HTML.
- Add a Content Security Policy as a second layer, since it limits what a missed case can do.
- Run a static analysis rule for these patterns in CI, and add a test that renders a hostile string and asserts escaped output.
- Never build template source from request data.
Quick check
Is html/template enough to stop all XSS in a Go app?
It covers values rendered through templates. Raw writes, trusted-type conversions such as template.HTML, and client-side script handling can still introduce XSS.
Can I escape the input once when I save it?
Escape at output, for the context where the value is used. A string safe in an HTML body is not automatically safe in a script or URL.
Sources
Last reviewed: 18 October 2026.