security_test.gno
6.15 Kb · 156 lines
1package home
2
3import (
4 "chain/runtime"
5 "encoding/base64"
6 "net/url"
7 "strconv"
8 "strings"
9 "testing"
10
11 "gno.land/p/nt/avl/v0"
12 "gno.land/p/nt/testutils/v0"
13 "gno.land/p/nt/uassert/v0"
14)
15
16// A guess arrives in the page link, so anyone can write one: none of it may
17// reach the rendered markdown.
18func TestGuessIsNeverEchoed(t *testing.T) {
19 setDay(launchDay)
20 uassert.True(t, strings.Contains(Render("wordle/1?guess=abc"), "> That guess is not five letters, A to Z."))
21 for _, guess := range []string{
22 "[claim your reward](https://evil.example)",
23 "x\n\n# OFFICIAL NOTICE",
24 "",
25 `<gno-form exec="Transfer"><gno-input name="to" /></gno-form>`,
26 } {
27 // Any rejected guess renders the very page a harmless rejected one does.
28 page := Render("wordle/1?guess=" + url.QueryEscape(guess))
29 uassert.Equal(t, Render("wordle/1?guess=abc"), page, "guess leaked into the page: "+guess)
30 }
31}
32
33func TestOnlyAdminWrites(cur realm, t *testing.T) {
34 for _, caller := range []runtime.Realm{
35 testing.NewUserRealm(testutils.TestAddress("mallory")),
36 testing.NewCodeRealm("gno.land/r/evil/relay"),
37 } {
38 testing.SetRealm(caller)
39 uassert.AbortsWithMessage(t, cur, "unauthorized", func() { Set(cross(cur), "header", "pwned") })
40 uassert.AbortsWithMessage(t, cur, "unauthorized", func() { Delete(cross(cur), "header") })
41 }
42 uassert.False(t, strings.Contains(Render(""), "pwned"))
43}
44
45func TestChartTextIsEscaped(cur realm, t *testing.T) {
46 testing.SetRealm(testing.NewUserRealm(admin))
47 old := Get(reviewActivitySlot)
48 Set(cross(cur), reviewActivitySlot, `title reviews; series <script>alert(1)</script>,a&b; 2026-01 1 2`)
49 out := renderChart(reviewActivitySlot)
50 raw, err := base64.StdEncoding.DecodeString(out[strings.Index(out, "base64,")+7 : len(out)-2])
51 uassert.NoError(t, err)
52 uassert.False(t, strings.Contains(string(raw), "<script>"))
53 uassert.True(t, strings.Contains(string(raw), "<script>"))
54 uassert.True(t, strings.Contains(string(raw), "a&b"))
55 Set(cross(cur), reviewActivitySlot, old)
56}
57
58func TestHeartsListIsPaged(t *testing.T) {
59 hearts = avl.NewTree()
60 for i := 0; i < 250; i++ {
61 hearts.Set(testutils.TestAddress("fan"+strconv.Itoa(i)).String(), int64(i))
62 }
63 first := Render("hearts")
64 uassert.Equal(t, heartsPerPage, strings.Count(first, "\n- "))
65 uassert.True(t, strings.Contains(first, "Page 1 of 3."))
66 uassert.True(t, strings.Contains(first, "[Next]("+home+":hearts/2)"))
67 uassert.False(t, strings.Contains(first, "[Previous]"))
68 last := Render("hearts/3")
69 uassert.Equal(t, 50, strings.Count(last, "\n- "))
70 uassert.True(t, strings.Contains(last, "[Previous]("+home+":hearts/2)"))
71 uassert.False(t, strings.Contains(last, "[Next]"))
72 for _, bad := range []string{"hearts/0", "hearts/4", "hearts/x", "hearts/-1"} {
73 uassert.True(t, strings.Contains(Render(bad), "There is no such page."), bad)
74 }
75 hearts = avl.NewTree()
76}
77
78func TestOrderSurvivesBadEntries(cur realm, t *testing.T) {
79 testing.SetRealm(testing.NewUserRealm(admin))
80 old := Get(orderSlot)
81 Set(cross(cur), orderSlot, "nosuchsection,,header")
82 page := Render("")
83 uassert.True(t, strings.HasPrefix(strings.TrimSpace(page), "I'm a developer engineer"))
84 Delete(cross(cur), orderSlot)
85 uassert.True(t, strings.Contains(Render(""), "This page is a contract"))
86 Set(cross(cur), orderSlot, old)
87}
88
89func TestQueryDoesNotBreakPages(t *testing.T) {
90 uassert.Equal(t, Render(""), Render("?utm_source=x"))
91 uassert.Equal(t, Render(""), Render("?fbclid=abc&x=1"))
92 uassert.Equal(t, Render("hearts"), Render("hearts?x=1"))
93 // The game keeps its query: it carries the guess.
94 setDay(launchDay)
95 uassert.True(t, strings.Contains(Render("wordle/1?guess=crane"), "CRANE"))
96}
97
98func TestLongPathIsRefused(t *testing.T) {
99 setDay(launchDay)
100 // The longest link the game writes: six guesses and a typed one.
101 longest := "wordle/99999/CRANE.SLATE.ROUTE.TRACE.QUERY.PROXY?guess=" + url.QueryEscape(" crane ")
102 uassert.True(t, len(longest) <= maxPathLen)
103 uassert.False(t, strings.HasPrefix(Render(longest), "# Not found"))
104 for _, p := range []string{
105 "wordle/1?" + strings.Repeat("a=&", 100),
106 "wordle/1/" + strings.Repeat(".", 300),
107 "hearts/" + strings.Repeat("9", 300),
108 } {
109 uassert.True(t, strings.HasPrefix(Render(p), "# Not found"), p[:20])
110 }
111}
112
113func TestHeroTextIsEditable(cur realm, t *testing.T) {
114 testing.SetRealm(testing.NewUserRealm(admin))
115 old := Get(heroSlot)
116 decode := func() string {
117 out := heroImage()
118 raw, err := base64.StdEncoding.DecodeString(out[strings.Index(out, "base64,")+7 : len(out)-1])
119 uassert.NoError(t, err)
120 return string(raw)
121 }
122 uassert.True(t, strings.Contains(decode(), ">David G.</text>"))
123
124 Set(cross(cur), heroSlot, "title Dave; subtitle <b>&co</b>; bogus x; tagline hello")
125 svg := decode()
126 uassert.True(t, strings.Contains(svg, ">Dave</text>"))
127 uassert.True(t, strings.Contains(svg, "><b>&co</b></text>"))
128 uassert.True(t, strings.Contains(svg, ">hello</text>"))
129 // A key the section leaves out keeps its default.
130 uassert.True(t, strings.Contains(svg, ">gno.land core contributor</text>"))
131 uassert.True(t, strings.HasPrefix(heroImage(), ")
132
133 Set(cross(cur), heroSlot, "alt a ]( trap")
134 uassert.True(t, strings.HasPrefix(heroImage(), ")
135
136 Set(cross(cur), heroSlot, old)
137}
138
139func TestSetActivity(cur realm, t *testing.T) {
140 oldPR, oldRev, oldAreas := Get(activitySlot), Get(reviewActivitySlot), Get(areasSlot)
141 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("mallory")))
142 uassert.AbortsWithMessage(t, cur, "unauthorized", func() { SetActivity(cross(cur), "x", "x", "x") })
143 uassert.Equal(t, oldPR, Get(activitySlot))
144
145 testing.SetRealm(testing.NewUserRealm(admin))
146 SetActivity(cross(cur), "title a; series s; 2026-01 1", "", "title c; slice 3 Docs")
147 uassert.Equal(t, "title a; series s; 2026-01 1", Get(activitySlot))
148 uassert.Equal(t, oldRev, Get(reviewActivitySlot)) // empty leaves it as it is
149 uassert.Equal(t, "title c; slice 3 Docs", Get(areasSlot))
150 out := renderChart(activitySlot)
151 raw, err := base64.StdEncoding.DecodeString(out[strings.Index(out, "base64,")+7 : len(out)-2])
152 uassert.NoError(t, err)
153 uassert.True(t, strings.Contains(string(raw), ">1 a</text>"))
154
155 SetActivity(cross(cur), oldPR, oldRev, oldAreas)
156}