templates: escape query value in ping page

elem-go does not escape attribute values, so the raw query reaches
the rendered HTML verbatim. Pre-escape with html.EscapeString to prevent
reflected XSS.

Updates #3157
This commit is contained in:
Kristoffer Dalby
2026-04-17 05:46:44 +00:00
parent 3a4af8cf87
commit f3eb9a7bba
3 changed files with 28 additions and 2 deletions

View File

@@ -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()))
}))

View File

@@ -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{

View File

@@ -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="`,
`"><script>alert(1)</script>`,
`<img src=x onerror=alert(1)>`,
}
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)
}
})
}
}