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