Interactive Playground
Experiment with the vulnerable code and security rule below. Edit the code to see how the rule detects different vulnerability patterns.
pathfinder scan --ruleset golang/GO-XSS-003 --project .About This Rule
Understanding the vulnerability and how it is detected
`io.WriteString(w, s)` writes the string `s` directly to the writer `w` as-is, performing no transformation of any kind. When `w` is an `http.ResponseWriter` and `s` contains user-supplied data, every HTML character in that data reaches the browser unescaped, enabling Cross-Site Scripting attacks.
**io.WriteString vs other write methods**: `io.WriteString` is a convenience function that writes a string to an `io.Writer` — it is functionally equivalent to `w.Write([]byte(s))`. Neither applies any escaping. This distinguishes it sharply from `html/template`, which applies context-aware escaping based on where a value is being inserted (HTML body, attribute, URL, JS context).
Common patterns that introduce this vulnerability:
1. **Direct user input write**: ```go io.WriteString(w, r.FormValue("name")) ```
2. **String concatenation before write**: ```go io.WriteString(w, "<b>Hello " + r.URL.Query().Get("user") + "</b>") ``` The concatenation does not escape the user value; `io.WriteString` writes the result raw.
3. **Indirect write via helper function**: ```go func respond(w io.Writer, msg string) { io.WriteString(w, msg) } // Called as: respond(w, r.FormValue("q")) ``` The taint flows through the intermediate function. This is why inter-procedural analysis is needed to catch this pattern.
4. **Writer passed through middleware**: A `ResponseWriter` wrapped by middleware still implements `io.Writer`. If tainted data is written to a buffered wrapper and the buffer is later flushed to the original ResponseWriter, the XSS payload reaches the client.
**Why io.WriteString appears in Go web handlers**: Go's `net/http` handler signature (`func(w http.ResponseWriter, r *http.Request)`) naturally leads developers to write responses using low-level I/O primitives. The `http.ResponseWriter` interface embeds `io.Writer`, making `io.WriteString`, `fmt.Fprintf`, and `w.Write()` all interchangeable from a type perspective. Developers writing simple handlers often reach for these rather than `html/template`, especially for small responses like error messages, where they underestimate the risk.
**Error handler XSS — a common false sense of security**: Error responses are a frequent location of XSS via `io.WriteString`: ```go http.Error(w, "Not found: "+r.URL.Path, 404) ``` Even `http.Error` ultimately writes the message to the ResponseWriter. If `r.URL.Path` contains HTML characters, they are written to the 404 page. Real-world scanners have found this pattern in production Go services where developers assumed "it's just an error page" absolved them of escaping requirements.
**Relationship between this rule and GO-XSS-002**: GO-XSS-002 covers `fmt.Fprintf/Fprintln/Fprint`; this rule covers `io.WriteString`. Both are raw writers with no HTML awareness. The remediation is identical: replace with `html/template` or pre-escape with `html.EscapeString`.
Security Implications
Potential attack scenarios if this vulnerability is exploited
Reflected XSS via URL Parameters
The most common io.WriteString XSS pattern: an HTTP handler reads a URL query parameter or form field and writes it back into the response (e.g., a "search results" page that shows what the user searched for). Attackers craft URLs with XSS payloads and distribute them via phishing emails, social media, or QR codes. Any victim who clicks the link executes the attacker's JavaScript in their browser.
Cookie and Session Token Theft
Injected JavaScript reads `document.cookie` and exfiltrates all cookies for the domain to an attacker-controlled endpoint via `new Image().src` or `fetch()`. If session cookies lack the `HttpOnly` flag, the attacker immediately hijacks the session without needing the user's password.
XSS in Error Pages
Error handlers that reflect the requested path or query string into 4xx/5xx responses are a common source of "low-severity" XSS that is exploitable in practice. A 404 page that says "The path /you-searched-for-X was not found" and reflects X without escaping is fully exploitable even if developers dismiss it as "just an error page."
Account Takeover via Admin Panel XSS
If io.WriteString XSS in an admin panel can be triggered by a regular user (e.g., by setting their username to a malicious payload stored in the database), visiting the admin panel with the user listed will execute the payload in the administrator's browser session, enabling privilege escalation.
How to Fix
Recommended remediation steps
- 1Replace io.WriteString(w, userInput) with html/template rendering for all HTML responses.
- 2If io.WriteString must be used, pre-escape all user values with html.EscapeString().
- 3Set Content-Type: text/html; charset=utf-8 explicitly — never rely on auto-detection.
- 4Add X-Content-Type-Options: nosniff to all responses to disable MIME sniffing.
- 5Apply Content-Security-Policy header as a defense-in-depth layer.
- 6Escape values in error pages and 4xx/5xx responses — they are equally exploitable.
- 7For JSON API handlers, use encoding/json rather than string formatting into io.WriteString.
- 8Pre-compile html/template at startup and reuse across requests for performance.
Detection Scope
How Code Pathfinder analyzes your code for this vulnerability
Tracks taint from HTTP request sources (net/http.Request.FormValue, URL.Query, gin.Context, echo.Context, fiber.Ctx) to io.WriteString calls where the first argument is an http.ResponseWriter. Global inter-procedural scope tracks taint through helper functions that accept io.Writer parameters.
Compliance & Standards
Industry frameworks and regulations that require detection of this vulnerability
References
External resources and documentation
Similar Rules
Explore related security rules for Go
XSS via Unsafe html/template Type Conversions
User input cast to template.HTML, template.CSS, template.JS, or template.URL bypasses Go's context-aware auto-escaping, allowing raw attacker payload to reach the browser.
XSS via fmt.Fprintf to http.ResponseWriter
User input flows into fmt.Fprintf/Fprintln/Fprint writing directly to ResponseWriter — fmt functions perform no HTML escaping, any user-controlled format argument renders as raw HTML in the browser.
Frequently Asked Questions
Common questions about XSS via io.WriteString to http.ResponseWriter
New feature
Get these findings posted directly on your GitHub pull requests
The XSS via io.WriteString to http.ResponseWriter rule runs in CI and posts inline review comments on the exact lines — no dashboard, no SARIF viewer.