Description
template.URL marks a value as a trusted URL. It does not validate input, and using it in html/template can bypass the URL-scheme filter applied to ordinary strings. Pass links containing user input to the template as ordinary strings and validate their destinations.
Potential impact
- A dangerous URL or an unescaped HTML attribute may allow scripts to run in the page.
- An attacker may read data accessible to the page or send requests with the user's privileges.
Remediation
- Do not convert user input to
template.URL. - Use the context-aware escaping in
html/templateinstead of concatenating HTML. - Explicitly restrict allowed destinations and encode query values with
url.QueryEscape. Query encoding does not replace HTML escaping or destination validation.
Examples
Before
go
package main
import (
"html/template"
"net/http"
)
// Unsafe: pass user input directly to template.URL()
func UserProfileHandler(w http.ResponseWriter, r *http.Request) {
// User input, e.g. /profile?redirect=https://example.com
redirect := r.URL.Query().Get("redirect")
// Input containing a single quote can break out of the href attribute
// Vulnerable: pass a string containing user input to template.URL()
url := "/go?next=" + redirect
// Converting to template.URL does not escape manually constructed HTML
safeURL := template.URL(url)
// Construct the HTML output by concatenating strings
html := "<a href='" + string(safeURL) + "'>계속하기</a>"
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
After
go
package main
import (
"html/template"
"net/http"
"net/url"
)
var tmpl = template.Must(template.New("link").Parse(`
<!DOCTYPE html>
<html>
<body>
<a href="{{.SafeNext}}">계속하기</a>
</body>
</html>
`))
// Validate input and use escaping with html/template
func UserProfileHandler(w http.ResponseWriter, r *http.Request) {
rawNext := r.URL.Query().Get("redirect")
// 1. Use only explicitly allowed internal destinations
if rawNext != "/home" && rawNext != "/profile" {
rawNext = "/home"
}
// 2. Encode the destination as the next query parameter value
encodedNext := url.QueryEscape(rawNext)
// 3. Pass data to the template for automatic escaping
data := struct {
SafeNext string
}{
SafeNext: "/go?next=" + encodedNext,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
}
Explanation: Before, a single quote in the input can break out of the href attribute and inject HTML. Appending javascript: after /go?next= does not itself create an executable URL scheme in this example. After, only /home and /profile are allowed, and the link passed to the template contains the encoded destination as a query value. The /go handler that performs the redirect must also validate the destination.