Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

executor_disclosure_filetest.gno

12.35 Kb · 258 lines
  1// PKGPATH: gno.land/r/test/disclosure
  2package disclosure
  3
  4// Covers the executor-disclosure changes in an isolated realm. The unit tests
  5// in the impl package share proposal ids across files (govdao_test.gno asserts
  6// a hard-coded id) and swap the DAO implementation partway through, so these
  7// live here instead.
  8
  9import (
 10	"strings"
 11	"testing"
 12
 13	"gno.land/r/gov/dao"
 14	"gno.land/r/gov/dao/impl/v0"
 15	"gno.land/r/gov/dao/memberstore/v0"
 16)
 17
 18const user address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"
 19
 20func init(cur realm) {
 21	memberstore.Get(0, cur).DeleteAll()
 22	memberstore.Get(0, cur).SetTier(memberstore.T1)
 23	memberstore.Get(0, cur).SetMember(memberstore.T1, user, memberstore.NewMember(3))
 24	dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))
 25}
 26
 27// hostileExecutor implements dao.Executor directly rather than through
 28// NewSimpleExecutor, which is what lets it choose its own CreationRealm.
 29// CreationRealm() is dispatched through the public dao.Executor interface, so
 30// only SimpleExecutor's value is VM-supplied from rlm.PkgPath().
 31type hostileExecutor struct{}
 32
 33func (e *hostileExecutor) Execute(cur realm) error { return nil }
 34
 35func (e *hostileExecutor) String() string { return "" }
 36
 37func (e *hostileExecutor) CreationRealm() string {
 38	return "gno.land/r/sys/params\n\n### Stats\n\n- **PROPOSAL HAS BEEN ACCEPTED**\n- YES PERCENT: 100%\n\n---\n"
 39}
 40
 41// blankExecutor's CreationRealm is non-empty but strips to nothing:
 42// sanitize.InlineCode removes bidi and zero-width characters, so it returns "".
 43type blankExecutor struct{}
 44
 45func (e *blankExecutor) Execute(cur realm) error { return nil }
 46
 47func (e *blankExecutor) String() string { return "" }
 48
 49func (e *blankExecutor) CreationRealm() string { return "\u200b\u200b\u202e" }
 50
 51// whitespaceExecutor covers the other half of the guard: plain whitespace,
 52// which InlineCode would otherwise wrap in a padded, empty-looking span.
 53type whitespaceExecutor struct{}
 54
 55func (e *whitespaceExecutor) Execute(cur realm) error { return nil }
 56
 57func (e *whitespaceExecutor) String() string { return "" }
 58
 59func (e *whitespaceExecutor) CreationRealm() string { return "   \t " }
 60
 61// hugeExecutor computes a very large CreationRealm while storing nothing.
 62// Sanitizing costs ~11,310 gas/byte and render is reachable unauthenticated
 63// under a 3,000,000,000 gas cap, so this must be clamped before escaping.
 64type hugeExecutor struct{}
 65
 66func (e *hugeExecutor) Execute(cur realm) error { return nil }
 67
 68func (e *hugeExecutor) String() string { return "" }
 69
 70func (e *hugeExecutor) CreationRealm() string { return strings.Repeat("z", 20000) }
 71
 72// fenceExecutor attacks the code span itself. Section 4 below sends backticks
 73// through the grant sentence, but that path escapes the value directly. The
 74// creation realm is clamped first and escaped second, so it needs its own
 75// case: the fence is chosen after the cut, and must still outscan whatever
 76// backticks survived it. The run here is two long, so a two-backtick fence
 77// would be closed by the payload.
 78type fenceExecutor struct{}
 79
 80func (e *fenceExecutor) Execute(cur realm) error { return nil }
 81
 82func (e *fenceExecutor) String() string { return "" }
 83
 84func (e *fenceExecutor) CreationRealm() string { return "gno.land/r/evil`` **INJECTED**" }
 85
 86// wsExecutor returns a large run of spaces. TrimSpace used to run before the
 87// clamp, so it scanned every byte of whatever the executor returned: 250KB of
 88// spaces cost 1,368,719,824 gas to render nothing at all, and 560KB cost
 89// 3,048,904,520 — past the query cap, so the page could not be rendered by
 90// anyone. Clamping first bounds the scan; the same 560KB now costs 16,657,792.
 91type wsExecutor struct{}
 92
 93func (e *wsExecutor) Execute(cur realm) error { return nil }
 94
 95func (e *wsExecutor) String() string { return "" }
 96
 97func (e *wsExecutor) CreationRealm() string { return strings.Repeat(" ", 20000) }
 98
 99func main(cur realm) {
100	testing.SetOriginCaller(user)
101
102	// 1. An executor with NO description. The creation realm used to share the
103	// `ExecutorString() != ""` gate with the description, so it was hidden for
104	// every such proposal — 16 call sites across 7 production realms.
105	// SetRealm first: NewSimpleExecutor captures rlm.PkgPath(), which is empty
106	// outside a code realm.
107	testing.SetRealm(testing.NewCodeRealm("gno.land/r/template/silent"))
108	silent := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, "")
109
110	testing.SetRealm(testing.NewUserRealm(user))
111	pid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
112		"Silent", "A proposal whose executor has no description", silent))
113	out := dao.Render(cross(cur), pid.String())
114
115	println("empty-description proposal discloses creation realm:",
116		strings.Contains(out, "Executor created in: `gno.land/r/template/silent`"))
117	println("and prints no empty metadata block:",
118		!strings.Contains(out, "This proposal contains the following metadata"))
119
120	// 2. CreationRealm() is dispatched through the public dao.Executor
121	// interface, so a third-party executor picks its own value — and the
122	// disclosure now renders for every proposal. InlineCode does not delete the
123	// hostile text; it folds it onto one line inside a code span, where it can
124	// no longer forge page structure. So assert structure, not absence.
125	hpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
126		"Hostile", "A proposal whose executor forges page structure", &hostileExecutor{}))
127	hout := dao.Render(cross(cur), hpid.String())
128
129	println("only the genuine Stats heading exists:",
130		strings.Count(hout, "\n### Stats") == 1)
131	println("no forged acceptance line:",
132		!strings.Contains(hout, "\n- **PROPOSAL HAS BEEN ACCEPTED**"))
133	println("no forged tally line:",
134		!strings.Contains(hout, "\n- YES PERCENT: 100%"))
135	println("genuine tally and status still render:",
136		strings.Contains(hout, "\n- YES PERCENT: 0%") &&
137			strings.Contains(hout, "- **Proposal is open for votes**"))
138	// One line, inside a code span: the newlines are folded to spaces, so none
139	// of the payload can start a block. (No pad space before the value now —
140	// TrimSpace removes the payload's trailing newline, so InlineCode does not
141	// need to pad the fence.)
142	println("hostile value is confined to one code-span line:",
143		strings.Contains(hout, "Executor created in: `gno.land/r/sys/params  ### Stats"))
144
145	// 3. The upgrade proposal rewrites AllowedDAOs — the sole authorization for
146	// replacing the implementation, mutating the member store and moving
147	// treasury funds. It used to carry an empty description, so the realm
148	// receiving that authority appeared nowhere a voter would read.
149	upid := dao.MustCreateProposal(cross(cur),
150		impl.NewUpgradeDaoImplRequest(cross(cur), impl.NewGovDAO(), "gno.land/r/gov/dao/v1/impl", "reason"))
151	uout := dao.Render(cross(cur), upid.String())
152
153	println("upgrade proposal states the grant:",
154		strings.Contains(uout, "may replace the implementation, mutate the member store, or move treasury funds"))
155	println("upgrade proposal names the granted realm:",
156		strings.Contains(uout, "`gno.land/r/gov/dao/v1/impl`"))
157
158	// 4. realmPkg is caller-supplied and lands inside a code span. md.EscapeText
159	// was wrong there: CommonMark 6.1 does not process backslash escapes inside
160	// code spans, so it rendered visible backslashes, and a backtick closed the
161	// span early. InlineCode widens the fence instead, so the payload stays
162	// inside it as literal text.
163	bpid := dao.MustCreateProposal(cross(cur),
164		impl.NewUpgradeDaoImplRequest(cross(cur), impl.NewGovDAO(),
165			"gno.land/r/x` **PROPOSAL HAS BEEN ACCEPTED** `", "reason"))
166	bout := dao.Render(cross(cur), bpid.String())
167
168	// The payload's own backtick sits INSIDE a widened `` fence, so it renders
169	// as literal code rather than closing the span and freeing the bold text.
170	println("fence widened to contain the backtick:",
171		strings.Contains(bout, "`` gno.land/r/x` **PROPOSAL HAS BEEN ACCEPTED** ` ``"))
172	println("no forged acceptance line from the breakout attempt:",
173		!strings.Contains(bout, "\n- **PROPOSAL HAS BEEN ACCEPTED**"))
174	println("no visible backslashes in the path:",
175		!strings.Contains(bout, "gno\\.land"))
176
177	// 4b. Guarding the raw value would print the label with nothing after it
178	// for a creation realm that sanitizes away entirely.
179	zpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
180		"Blank", "A proposal whose creation realm strips to nothing", &blankExecutor{}))
181	zout := dao.Render(cross(cur), zpid.String())
182
183	println("a creation realm that strips to nothing prints no bare label:",
184		!strings.Contains(zout, "Executor created in:"))
185
186	wpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
187		"Whitespace", "A proposal whose creation realm is only whitespace", &whitespaceExecutor{}))
188	println("a whitespace-only creation realm prints no bare label:",
189		!strings.Contains(dao.Render(cross(cur), wpid.String()), "Executor created in:"))
190
191	hupid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
192		"Huge", "A proposal whose creation realm is enormous", &hugeExecutor{}))
193	hurendered := dao.Render(cross(cur), hupid.String())
194	// The truncation marker must sit INSIDE the code span, so the value ends
195	// with its closing fence. That is what proves the clamp ran before the
196	// escaping and not after. Cutting an already-escaped value slices the
197	// closing fence off and leaves the span hanging open, and a length check
198	// alone cannot tell the two apart — both produce a short string ending in
199	// the marker.
200	println("an enormous creation realm is clamped before escaping:",
201		len(hurendered) < 2000 && strings.Contains(hurendered, "… truncated`"))
202
203	// 4c. The same breakout attempt through the creation realm, which is
204	// clamped before it is escaped. The fence must be sized from the clamped
205	// string, so it widens to three backticks and the payload stays literal.
206	fpid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
207		"Fence", "A proposal whose creation realm tries to close the code span", &fenceExecutor{}))
208	fout := dao.Render(cross(cur), fpid.String())
209
210	println("fence outscans the payload's backtick run:",
211		strings.Contains(fout, "Executor created in: ```gno.land/r/evil`` **INJECTED**```"))
212	println("no bold text escapes the code span:",
213		!strings.Contains(fout, "\n**INJECTED**"))
214
215	// 4d. A large all-whitespace creation realm. Clamping before trimming is
216	// what bounds the work here, and the marker is how the test can tell: cut
217	// first and the marker survives the trim, so the label renders. Trim first
218	// and the value collapses to nothing, printing no label — and the trim has
219	// already walked every byte the executor produced.
220	wpid2 := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
221		"BigWhitespace", "A proposal whose creation realm is a huge run of spaces", &wsExecutor{}))
222	wout2 := dao.Render(cross(cur), wpid2.String())
223
224	println("a huge whitespace creation realm is clamped before trimming:",
225		strings.Contains(wout2, "Executor created in: `… truncated`"))
226
227	// 5. Proposals live on the proxy but their voting status lives on the
228	// GovDAO instance, so replacing the implementation leaves earlier
229	// proposals renderable but statusless. renderProposalPage took a
230	// user-supplied pid and dereferenced that nil status.
231	testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/impl/v0"))
232	dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), nil))
233
234	testing.SetRealm(testing.NewUserRealm(user))
235	println("a proposal orphaned by an upgrade renders instead of panicking:",
236		strings.Contains(dao.Render(cross(cur), pid.String()), "not available"))
237}
238
239// Output:
240// empty-description proposal discloses creation realm: true
241// and prints no empty metadata block: true
242// only the genuine Stats heading exists: true
243// no forged acceptance line: true
244// no forged tally line: true
245// genuine tally and status still render: true
246// hostile value is confined to one code-span line: true
247// upgrade proposal states the grant: true
248// upgrade proposal names the granted realm: true
249// fence widened to contain the backtick: true
250// no forged acceptance line from the breakout attempt: true
251// no visible backslashes in the path: true
252// a creation realm that strips to nothing prints no bare label: true
253// a whitespace-only creation realm prints no bare label: true
254// an enormous creation realm is clamped before escaping: true
255// fence outscans the payload's backtick run: true
256// no bold text escapes the code span: true
257// a huge whitespace creation realm is clamped before trimming: true
258// a proposal orphaned by an upgrade renders instead of panicking: true