Repository: gnolang/gno (Go)

Issue:
contribs/gnodev/pkg/emitter: use html/template not text/template to avoid any HTML serving issues or future unescaped XSS attacks: minor
**_This is a currently minor issue_** given that the code inside middleware.go is not attacker controlled but really when starting up a server
https://github.com/gnolang/gno/blob/d2813f87533d69409201c2ee8e87c9bcff6c1719/contribs/gnodev/pkg/emitter/middleware.go#L24
https://github.com/gnolang/gno/blob/d2813f87533d69409201c2ee8e87c9bcff6c1719/contribs/gnodev/pkg/emitter/middleware.go#L86-L89

but it is best practice to using html/template not text/template for any HTML related code. Cross site scripting (XSS) attacks occur when injected code is not correctly escaped and I gave a proof of concept for the dangers of using text/template instead of html/template at https://cyber.orijtech.com/advisory/go-xss-concat-text-template

## Exhibit
If an attacker ever got a hold of .RemoteAddr before it was generated or if the scripts evolved to later allow user input here is an exhibit of how text/template can go awry https://go.dev/play/p/dTAacWXE9uC

```go
package main

import (
	htmltmpl "html/template"
	"os"
	texttmpl "text/template"
)

var naiveTmpl = texttmpl.Must(texttmpl.New("reloadscript").Parse("const ws = new WebSocket('ws://{{- .Remote -}}');"))
var goodTmpl = htmltmpl.Must(htmltmpl.New("reloadscript").Parse("const ws = new WebSocket('ws://{{- .Remote -}}');"))

func main() {
	m := map[string]string{
		"Remote": `localhost:9999');alert('pwned`,
	}

	_ = naiveTmpl.Execute(os.Stdout, m)
	println("")
	_ = goodTmpl.Execute(os.Stdout, m)
}
```

which when ran produces

```shell
const ws = new WebSocket('ws://localhost:9999');alert('pwned');
const ws = new WebSocket('ws://localhost:9999&#39;);alert(&#39;pwned');
```

of which the first one uses text/template and will get your pwned as I can now inject arbitrary code and as a proof of concept will pop up an alert telling you `pwned` but the imagination can go wild with what you could do

but the html/template code rightfully caught the problem and produced
```
const ws = new WebSocket('ws://localhost:9999&#39;);alert(&#39;pwned');
```
which will fail to connect entirely.


Discussion:

Patch:
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/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 -}}');


Test patch:
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)
+		})
+	}
+}