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_operator_authlist_capability_filetest.gno

3.98 Kb · 98 lines
 1// PKGPATH: gno.land/r/zzoperatorauth
 2//
 3// Regression pin for the per-operator auth-list capability leak
 4// (reported by @thehowl; pre-existing, fixed here because it is the same
 5// class as the governance capability leak, and the same audit that found
 6// that one should have found it).
 7//
 8// THE HOLE. `Valoper.Auth() *authorizable.Authorizable` was exported and
 9// `GetByAddr` is exported and non-crossing. Valoper is returned BY
10// VALUE, but `auth` is a pointer field, so the copy shared the realm's
11// live Authorizable. Authorizable's gates read
12// `rlm.Previous().Address()`, so inside a hostile realm's frame
13// Previous() is whoever called it. A valoper OPERATOR merely calling any
14// function of a hostile realm (faucet, airdrop, mint) let that realm run
15//
16//	valopers.GetByAddr(operator).Auth().AddToAuthList(0, cur, attacker)
17//
18// with Previous() == the operator, i.e. the Authorizable's own owner.
19// The write was accepted and PERSISTED. From the next transaction on,
20// the attacker acted alone: UpdateKeepRunning to drain the validator,
21// UpdateSigningKey to rotate its consensus signing key.
22//
23// THE FIX is the one layer 2 applies to the realm's governance
24// authority: never export the live capability. `Auth()` is removed;
25// `AuthOwner()` copies out an address.
26//
27// WHAT THIS FILE PINS, honestly scoped. The raw-handle attack is a
28// compile-time absence now, so — like the foreign-realm filetest —
29// reverting the fix fails this on a build error rather than an
30// assertion. What it
31// drives at RUNTIME is the property that makes the removal safe: the
32// exported wrapper is NOT an equivalent route. There `cur.Previous()` is
33// the hostile realm rather than the operator, so the superuser check
34// refuses, and it refuses even in the exact scenario that used to work —
35// the operator calling the hostile realm themselves.
36//
37// NOTE for whoever edits valopers next: re-adding any exported accessor
38// that returns *authorizable.Authorizable (or the *ownable.Ownable
39// inside it) reopens this, and this file would still compile.
40package zzoperatorauth
41
42import (
43	"chain"
44	"testing"
45
46	"gno.land/p/nt/testutils/v0"
47	"gno.land/r/gnops/valopers"
48)
49
50var (
51	operator = testutils.TestAddress("operator")
52	attacker = chain.PackageAddress("gno.land/r/zzoperatorauth")
53	pubKey   = "gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p"
54)
55
56// Pwn is an ordinary exported function of a hostile realm — the airdrop
57// the operator was told to claim. It needs no privilege of its own; it
58// used to just read the public handle and write through it.
59func Pwn(cur realm, victim address) {
60	// The attack the fix removes. Uncommenting must not compile:
61	//
62	//	valopers.GetByAddr(victim).Auth().AddToAuthList(0, cur, attacker)
63	//	                           ^^^^^^ undefined
64	//
65	// What remains is the exported wrapper. It is not equivalent: it
66	// derives the principal from THIS frame, where Previous() is
67	// gno.land/r/zzoperatorauth and not the operator. Aborts.
68	valopers.AddToAuthList(cross(cur), victim, attacker)
69	println("UNREACHABLE: hostile realm joined the operator's auth list")
70}
71
72func main(cur realm) {
73	// The operator registers their profile normally.
74	testing.SetOriginCaller(operator)
75	testing.SetRealm(testing.NewUserRealm(operator))
76	valopers.Register(cross(cur), "Op",
77		"a valid description for the operator profile", "cloud", operator, pubKey)
78
79	// The auth list is owned by the operator, and AuthOwner hands back
80	// an address — a value, not a capability.
81	println("owner is operator:",
82		valopers.GetByAddr(operator).AuthOwner() == operator)
83	println("attacker is not owner:",
84		valopers.GetByAddr(operator).AuthOwner() != attacker)
85
86	// The operator calls the hostile realm once. This single step used
87	// to be the whole attack.
88	testing.SetOriginCaller(operator)
89	testing.SetRealm(testing.NewUserRealm(operator))
90	Pwn(cross(cur), operator)
91}
92
93// Output:
94// owner is operator: true
95// attacker is not owner: true
96
97// Error:
98// authorizable: caller is not superuser