Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions gno.land/pkg/gnoweb/components/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ var funcMap = template.FuncMap{}
var tmpl = template.New("web")

func registerCommonFuncs(funcs template.FuncMap) {
// NOTE: this method does NOT escape HTML, use with caution
funcs["noescape_string"] = func(in string) template.HTML {
return template.HTML(in) //nolint:gosec
}
// NOTE: this method does NOT escape HTML, use with caution
funcs["noescape_bytes"] = func(in []byte) template.HTML {
return template.HTML(in) //nolint:gosec
}
// NOTE: this method does NOT escape HTML, use with caution
// Render Component element into raw html element
funcs["render"] = func(comp Component) (template.HTML, error) {
Expand Down
2 changes: 1 addition & 1 deletion gno.land/pkg/gnoweb/components/ui/toc.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
{{ define "ui/toc_realm" }}
<ul class="b-toc">{{- range .Items }}
<li>
<a href="{{ .Anchor }}"> {{ .Title | noescape_bytes }}
<a href="{{ .Anchor }}"> {{ printf "%s" .Title }}
</a>
{{ if .Items }} {{ template "ui/toc_realm" . }} {{ end }}
</li>
Expand Down
96 changes: 95 additions & 1 deletion gno.land/pkg/gnoweb/components/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

"github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown"
"github.com/gnolang/gno/gnovm/pkg/doc"
"github.com/stretchr/testify/assert"
)

func TestSourceView(t *testing.T) {
Expand Down Expand Up @@ -168,6 +169,99 @@ func TestRealmView(t *testing.T) {
assert.NoError(t, view.Render(io.Discard))
}

func TestRealmViewTOCXSSPrevention(t *testing.T) {
tests := []struct {
name string
tocTitle []byte
tocID []byte
mustNotContain []string
mustContain []string
}{
{
name: "HTML entities in TOC title",
tocTitle: []byte("&lt;script&gt;alert('XSS')&lt;/script&gt; Heading"),
tocID: []byte("heading"),
mustNotContain: []string{"<script>alert", "<script>"},
mustContain: []string{"&amp;lt;script&amp;gt;"},
},
{
name: "Image tag with onerror via entities",
tocTitle: []byte("&lt;img src=x onerror=alert(1)&gt;"),
tocID: []byte("img"),
mustNotContain: []string{},
mustContain: []string{"&amp;lt;img"},
},
{
name: "SVG with onload via entities",
tocTitle: []byte("&lt;svg onload=alert(1)&gt;"),
tocID: []byte("svg"),
mustNotContain: []string{},
mustContain: []string{"&amp;lt;svg"},
},
{
name: "Numeric HTML entities",
tocTitle: []byte("&#60;script&#62;alert(1)&#60;/script&#62;"),
tocID: []byte("numeric"),
mustNotContain: []string{"<script"},
mustContain: []string{"&amp;#60;script"},
},
{
name: "Normal heading with ampersand",
tocTitle: []byte("API & SDK"),
tocID: []byte("api-sdk"),
mustNotContain: []string{},
mustContain: []string{"API &amp; SDK"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a RealmView with potentially malicious TOC item
content := NewReaderComponent(strings.NewReader("test content"))
tocItems := &RealmTOCData{
Items: []*markdown.TocItem{
{Title: tt.tocTitle, ID: tt.tocID},
},
}
data := RealmData{
ComponentContent: content,
TocItems: tocItems,
}

view := RealmView(data)

var buf strings.Builder
err := view.Render(&buf)
assert.NoError(t, err, "expected no error rendering view")

rendered := buf.String()

tocStart := strings.Index(rendered, `<ul class="b-toc">`)
tocEnd := strings.LastIndex(rendered, `</ul>`)
if tocStart == -1 || tocEnd == -1 {
t.Fatal("could not find TOC in rendered HTML")
}
tocHTML := rendered[tocStart : tocEnd+5]

for _, danger := range tt.mustNotContain {
if strings.Contains(tocHTML, danger) {
t.Errorf("Found unescaped dangerous pattern %q in TOC HTML.\n"+
"TOC HTML: %s",
danger, tocHTML)
}
}

for _, safe := range tt.mustContain {
if !strings.Contains(tocHTML, safe) {
t.Errorf("Expected escaped pattern %q not found in TOC HTML.\n"+
"Title: %s\nTOC HTML: %s",
safe, string(tt.tocTitle), tocHTML)
}
}
})
}
}

func TestHelpView(t *testing.T) {
functions := []*doc.JSONFunc{
{Name: "Func1", Params: []*doc.JSONField{{Name: "param1"}}},
Expand Down
Loading