From 99f802544bee1e2082c5a3ce2368fe2bd0f11af1 Mon Sep 17 00:00:00 2001
From: Emmanuel T Odeke <emmanuel@orijtech.com>
Date: Sat, 18 Jan 2025 17:33:15 -0800
Subject: [PATCH] fix(contribs/gnodev/pkg/emitter): use html/template not
 text/template for HTML generation

This change uses html/template instead of text/template for HTML
generation and also locks in tests to detect such subtle regressions
and thus help prevent future cross-side scripting (XSS) attacks
if later the scripts evolve and take in user input.

Fixes #3544
---
 contribs/gnodev/pkg/emitter/middleware.go     |  2 +-
 .../gnodev/pkg/emitter/middleware_test.go     | 43 +++++++++++++++++++
 .../gnodev/pkg/emitter/static/hotreload.js    |  4 +-
 3 files changed, 47 insertions(+), 2 deletions(-)
 create mode 100644 contribs/gnodev/pkg/emitter/middleware_test.go

diff --git a/contribs/gnodev/pkg/emitter/middleware.go b/contribs/gnodev/pkg/emitter/middleware.go
index 9c53cfe158e..e4def43f919 100644
--- a/contribs/gnodev/pkg/emitter/middleware.go
+++ b/contribs/gnodev/pkg/emitter/middleware.go
@@ -5,10 +5,10 @@ import (
 	_ "embed"
 	"encoding/json"
 	"fmt"
+	"html/template"
 	"net/http"
 	"strings"
 	"sync"
-	"text/template"
 
 	"github.com/gnolang/gno/contribs/gnodev/pkg/events"
 )
diff --git a/contribs/gnodev/pkg/emitter/middleware_test.go b/contribs/gnodev/pkg/emitter/middleware_test.go
new file mode 100644
index 00000000000..bed7ddd0e75
--- /dev/null
+++ b/contribs/gnodev/pkg/emitter/middleware_test.go
@@ -0,0 +1,43 @@
+package emitter
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestMiddlewareUsesHTMLTemplate(t *testing.T) {
+	tests := []struct {
+		name   string
+		remote string
+		want   string
+	}{
+		{"normal remote", "localhost:9999", "const ws = new WebSocket('ws://localhost:9999');"},
+		{"xss'd remote", `localhost:9999');alert('pwned`, "const ws = new WebSocket('ws://localhost:9999&#39;);alert(&#39;pwned');"},
+	}
+
+	// As the code revolves, add more search patterns here.
+	reWebsocket := regexp.MustCompile("const ws = new WebSocket[^\n]+")
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			rec := httptest.NewRecorder()
+			mdw := NewMiddleware(tt.remote, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+				rw.Header().Set("Content-Type", "text/html")
+				fmt.Fprintf(rw, "<body></body>")
+			}))
+			rec.Header().Set("Content-Type", "text/html")
+			req := httptest.NewRequest("GET", "https://gno.land/example", nil)
+			mdw.ServeHTTP(rec, req)
+
+			targets := reWebsocket.FindAllString(rec.Body.String(), -1)
+			require.True(t, len(targets) > 0)
+			body := targets[0]
+			require.Equal(t, body, tt.want)
+		})
+	}
+}
diff --git a/contribs/gnodev/pkg/emitter/static/hotreload.js b/contribs/gnodev/pkg/emitter/static/hotreload.js
index aabad4f341c..28e47c1ea15 100644
--- a/contribs/gnodev/pkg/emitter/static/hotreload.js
+++ b/contribs/gnodev/pkg/emitter/static/hotreload.js
@@ -1,6 +1,8 @@
 (function() {
     // Define the events that will trigger a page reload
-    const eventsReload = {{ .ReloadEvents | json }};
+    const eventsReload = [
+        {{range .ReloadEvents}}'{{.}}',{{end}}
+    ];
     
     // Establish the WebSocket connection to the event server
     const ws = new WebSocket('ws://{{- .Remote -}}');