diff --git a/hscontrol/debug.go b/hscontrol/debug.go index 096ea25b..c27b3975 100644 --- a/hscontrol/debug.go +++ b/hscontrol/debug.go @@ -361,7 +361,6 @@ func (h *Headscale) debugHTTPServer() *http.Server { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) - //nolint:gosec // elem-go auto-escapes all attribute values; no XSS risk. _, _ = w.Write([]byte(templates.PingPage(query, result, nodes).Render())) })) diff --git a/hscontrol/templates/ping.go b/hscontrol/templates/ping.go index f7e2c63e..f4d2fb3c 100644 --- a/hscontrol/templates/ping.go +++ b/hscontrol/templates/ping.go @@ -2,6 +2,7 @@ package templates import ( "fmt" + "html" "strings" "time" @@ -101,7 +102,7 @@ func pingForm(query string) *elem.Element { elem.Input(attrs.Props{ attrs.Type: "text", attrs.Name: "node", - attrs.Value: query, + attrs.Value: html.EscapeString(query), attrs.Placeholder: "Node ID, IP, or hostname", attrs.Autofocus: "true", attrs.Style: styles.Props{ diff --git a/hscontrol/templates/ping_test.go b/hscontrol/templates/ping_test.go new file mode 100644 index 00000000..3e80cf8f --- /dev/null +++ b/hscontrol/templates/ping_test.go @@ -0,0 +1,26 @@ +package templates + +import ( + "strings" + "testing" +) + +// TestPingPageEscapesQuery asserts hostile query values cannot break out of +// the input's value attribute. elem-go does not escape attribute values, so +// the template must escape before rendering. +func TestPingPageEscapesQuery(t *testing.T) { + payloads := []string{ + `" autofocus onfocus=alert(1) x="`, + `">`, + ``, + } + + for _, p := range payloads { + t.Run(p, func(t *testing.T) { + out := PingPage(p, nil, nil).Render() + if strings.Contains(out, p) { + t.Fatalf("unescaped payload rendered verbatim: %q", p) + } + }) + } +}