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

z_foreign_realm_capability_filetest.gno

3.80 Kb · 90 lines
 1// PKGPATH: gno.land/r/zzattacker
 2//
 3// Consumer-level regression pin for the governance capability leak.
 4//
 5// The package-level pins live in p/moul/authz. This is the consumer-level
 6// one: an unrelated realm must not be able to drive a valopers privileged
 7// action.
 8//
 9// Before the fix this file could not exist as a negative test — the
10// realm exported NewInstructionsProposalCallback, a crossing closure
11// declared in valopers. The VM mints a crossing frame's `cur` from the
12// CALLEE's declaring package, so invoking it from anywhere ran
13// updateInstructions with cur.Address() == the ContractAuthority's
14// contractAddr. The contract-identity gate accepted, the init.gno
15// handler ran the action, and `instructions` was rewritten with no
16// proposal and no vote:
17//
18//	err: undefined
19//	PWNED-BY-ATTACKER
20//
21// The capability is now sealed inside a dao.ProposalRequest, whose
22// executor field is unexported with no accessor. This file asserts the
23// two halves of that: the leak is gone from the type system, and the
24// request an attacker can still construct is inert in their hands.
25//
26// NOTE for whoever edits valopers next: the property under test is
27// "no exported function returns anything carrying this realm's frame
28// identity". Re-exporting a func(realm) error, or returning a
29// dao.Executor (whose Execute method is exported and directly
30// invocable), reopens the hole even though this file would still
31// compile.
32package zzattacker
33
34import (
35	"strings"
36	"testing"
37
38	"gno.land/p/nt/testutils/v0"
39	"gno.land/r/gnops/valopers"
40	"gno.land/r/gov/dao"
41	daov3init "gno.land/r/gov/dao/init/v0"
42)
43
44// A real GovDAO exists and the attacker is not in it.
45var govdaoMember address = testutils.TestAddress("govdao-member")
46
47func init(cur realm) {
48	daov3init.InitWithUsers(cross(cur), govdaoMember)
49}
50
51func main(cur realm) {
52	// 1. The bare capability is gone. Uncommenting this must not compile:
53	//
54	//	cb := valopers.NewInstructionsProposalCallback("PWNED-BY-ATTACKER")
55	//	cb(cross(cur))
56	//
57	// What remains is the sealed request. An unrelated realm may still
58	// build one — construction is harmless — but dao.ProposalRequest
59	// exposes only Title/Description/Filter, so there is no way to reach
60	// the executor and run it. Only GovDAO executing the proposal can.
61	testing.SetRealm(testing.NewCodeRealm("gno.land/r/zzattacker"))
62
63	req := valopers.NewInstructionsProposalRequest(cross(cur), "PWNED-BY-ATTACKER")
64	println("attacker built a request:", req.Title())
65
66	// 2. The privileged write did not happen: the instructions are still
67	//    the genesis text, not the attacker's. Under the exported callback
68	//    this line already printed "defaced: true".
69	rendered := valopers.Render("")
70	println("instructions defaced:", strings.Contains(rendered, "PWNED-BY-ATTACKER"))
71	println("instructions intact:", strings.Contains(rendered, "Welcome to the **Valopers** realm"))
72
73	// 3. Runtime pin, not merely a symbol-absence one. dao.ProposalRequest
74	//    exposes Title/Description/Filter and nothing that yields the
75	//    executor, so the one remaining route for a holder is to ask
76	//    GovDAO to adopt the request. A hostile realm cannot: this aborts.
77	//    (A GovDAO *member* still can, and then it needs the votes — that
78	//    is the legitimate path, covered by
79	//    r/gnops/valopers/proposal/filetests/z_governed_instructions_filetest.gno.)
80	dao.MustCreateProposal(cross(cur), req)
81	println("UNREACHABLE: outsider got the proposal adopted")
82}
83
84// Output:
85// attacker built a request: /r/gnops/valopers: Update instructions
86// instructions defaced: false
87// instructions intact: true
88
89// Error:
90// proposal creation must be done directly by a user or through the r/gov/dao proxy. caller realm: realm{gno.land/r/gov/dao:g1p84dvfh4wrplyxx4zsmy77a8rxcnjup2j7zv5r}; caller's previous: realm{gno.land/r/zzattacker:g1q577nx395ehscyn9ucutvfpj03a5tuk7x4v3ad}