To prevent cross-site scripting in a Go web app, render HTML with the html/template package, which escapes output by context, not with text/template. Keep untrusted data out of the package's typed "already safe" strings, pass JSON as data, and add a Content Security Policy as a second layer.
Why does the import matter so much?
Go ships two template packages with the same API. According to the Go documentation, text/template does no contextual escaping, so a script payload comes out raw. html/template works out whether each action sits in HTML text, an attribute, a URL, CSS or JavaScript and attaches the matching escaper at parse time. Switching is mostly an import change, which is exactly why a wrong import is easy to miss in review.
The package's security model is plain: template authors are trusted, and the data given to Execute is not. It tries to keep three properties: structure (data cannot change tags or string boundaries), code effect (only author-specified code runs) and least surprise.
What does the escaping do in practice?
In an HTML body, angle brackets and quotes in a value become entities. In a URL attribute, a value with a disallowed scheme is replaced by the marker #ZgotmplZ at runtime. In JavaScript value contexts, non-string data is marshaled as JSON, and strings are escaped. If you ever see #ZgotmplZ in output, do not disable the filter. Fix the data or the template.
Which typed strings are dangerous?
The package defines types such as HTML, HTMLAttr, JS, JSStr, CSS, URL and Srcset. They tell the package the content is already safe, so it is included verbatim. The documentation warns repeatedly that the content must come from a trusted source. HTML should not hold third-party HTML or markup with unclosed tags or comments. JS is the riskiest, and untrusted JSON should not go into it: unmarshal it and pass the resulting value instead. URL exists to let trusted dynamic javascript: URLs through, since they are filtered by default.
In code review, grep for conversions to these types and ask where each value came from. A constructed example: a handler wraps a user's "about me" field in template.HTML so that line breaks survive. That one conversion turns the field into stored XSS. Escape it, then convert newlines safely, or sanitize with a dedicated HTML sanitizer.
What else do OWASP's rules add?
OWASP's cheat sheet says to encode for the exact context, quote attribute values, and avoid placing untrusted data in dangerous spots altogether: directly inside script or style blocks, HTML comments, tag or attribute names, event handler attributes, and calls such as eval, setTimeout or setInterval with strings. Encoding does not make those places safe. It also says a CSP should supplement output encoding, not replace it, and should be tailored to the application.
What to do
- Search the code base for text/template imports that render HTML and change them.
- Search for template.HTML, template.JS, template.URL and similar conversions. Justify or remove each.
- Pass structured data to script blocks as values, not as concatenated strings.
- Validate redirect and link targets, and allow only http and https.
- Set a Content Security Policy and test it in report mode first.
- Add a test that renders a script payload through every template and checks the output is escaped.
Safeguard offers SAST scanning and has a demo at /demos/sast-code-scan if you want to see how source findings are presented.
Quick check
Is html/template enough on its own?
It covers output encoding well, but typed safe strings, unsafe sinks and missing validation can still open holes. Keep a CSP as defense in depth.
What does #ZgotmplZ mean?
It is the runtime marker html/template inserts when a value is unsafe in its context, such as a URL with a disallowed scheme.
Sources
Last reviewed: 18 October 2026.