VNX-GO-020: Use of template.HTML with potential user input
Detects template.HTML, HTMLAttr, JS, CSS, URL and Srcset conversions applied to request data, markup built with Sprintf or concatenation, a function parameter, or a variable assigned from one of those. Constant markup, sanitised or escaped values and executed template output are not reported.
| Field | Value |
|---|---|
| Rule ID | VNX-GO-020 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Go |
| CWE | CWE-79 |
| CAPEC | CAPEC-63 |
| ATT&CK | T1059.007 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | xss template html |
| Source | internal/sast/rules/vnx-go-020.rego |
Overview
This rule flags instances where template.HTML is used to mark a string as safe HTML without proper validation or sanitization. When user-controlled data is passed to template.HTML, it can lead to Cross-Site Scripting (XSS) vulnerabilities because the HTML template engine will treat the input as safe and render it directly without escaping.
This maps to CWE-79: Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’).
Why This Matters
The template.HTML function in Go’s html/template package tells the template engine that the provided string is safe HTML and should not be escaped. When this function is used with user-controlled input (such as form data, URL parameters, or HTTP headers), an attacker can inject malicious JavaScript that will execute in victims’ browsers.
XSS vulnerabilities can lead to session hijacking, credential theft, defacement, or malware distribution. The vulnerability is particularly dangerous because it exploits the trust users have in a legitimate website.
What Gets Flagged
The rule reports a trusted-content conversion (template.HTML, HTMLAttr,
JS, JSStr, CSS, URL, Srcset) whose argument is request data, markup
built from a variable, a function parameter, or a variable assigned from one
of those. Constant markup and sanitised values are not reported.
// FLAGGED: a request value marked safe
bio := r.FormValue("bio")
data["Bio"] = template.HTML(bio)
// FLAGGED: markup formatted from the request, two lines away
notFound := fmt.Sprintf("<b><i>%s</i></b> was not found", term)
data["Message"] = template.HTML(notFound)
// FLAGGED: a helper that marks whatever its caller passes as safe
"raw": func(s string) template.HTML { return template.HTML(s) },
// NOT FLAGGED: developer-written markup, or sanitised user markup
data["Banner"] = template.HTML(`<span class="beta">Beta</span>`)
data["Body"] = template.HTML(policy.Sanitize(body))
Remediation
-
Avoid using template.HTML with untrusted input: Instead, let the template engine automatically escape your data by passing it directly:
// SAFE: Let template engine escape the input tmpl.Execute(w, userInput) // Will be HTML-escaped automatically -
Sanitize input before marking as safe: If you must allow certain HTML tags, use a proper HTML sanitization library:
import "github.com/microcosm-cc/bluemonday" // SAFE: Sanitize before marking as HTML sanitizer := bluemonday.UGCPolicy() safeHTML := template.HTML(sanitizer.Sanitize(userInput)) -
Use template functions for specific needs: Rather than bypassing the safety mechanisms entirely, create custom template functions:
func safeLink(url string) template.URL { // Validate and sanitize URL here return template.URL(url) } // Then in template: <a href="{{.Link | safeLink}}">Click</a> -
Consider the context: Remember that different contexts require different escaping (HTML vs JavaScript vs CSS vs URL). The
html/templatepackage handles HTML context automatically when you don’t usetemplate.HTML.
References
- CWE-79: Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’)
- OWASP Application Security Verification Standard v4.0 – V1.2.1 Output Encoding for HTTP Responses
- OWASP Cross Site Scripting Prevention Cheat Sheet
- Go html/template package documentation
- Bluemonday HTML Sanitizer
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-go-020
// nosec vnx-go-020
Or record an ignore rule: vulnetix ignore add --rule vnx-go-020 --reason "...". See Suppressing findings.