上篇文章中,我们创造了两个合约send.code
和recv.code
,并且演示了如何从send.code
合约向recv.code
合约发送inline action
。那么这种方式有没有安全问题呢?狼人杀游戏所暴出的eosio.code
漏洞又是怎么回事呢?
回顾
上篇中,我们给user
账号新增了一个许可权限sendp
,执行了如下三步操作:
- 把
user
的sendp
许可,授权给send.code
的eosio.code
许可- 把user的这个
sendp
许可和recv.code
的receive
这个action handler关联起来- 因为新建了一个
sendp
许可,所以为了能使用该许可向send.code
发送send
action,我们还需要把它和send.code
的send
action关联起来。
这种方式,相比之前的EOS版本(准确是说是dawn4.0以前)合约可以随意转移用户资产的情况,增加了一个用户授权的过程。这个需要用户确认授权的步骤,的确要安全一点。那么之前狼人杀游戏所暴出的eosio.code
漏洞是怎么回事呢?
狼人杀游戏使用eosio.code
的方式与我们上面的方式略有不同。它没有为用户创建一个单独的许可,而是直接向用户索取active
许可。
我们下面模拟一下狼人杀游戏中使用eosio.code
的方式:
模拟狼人杀中的授权
把user
的active
权限授予合约send.code
的eosio.code
许可
命令如下:
~ cleos set account permission user active '{"threshold": 1,"keys": [{"key":"EOS83sN8bfKGk3jTBezN41UN7LfXSVFa1w3YQcGApE67J26t3HLcr", "weight":1}],"accounts": [{"permission":{"actor":"send.code","permission":"eosio.code"},"weight":1}]}' owner -p user@owner
executed transaction: d9d392e93833e0835e7be4fc834b5aa707aa2d445314c0ccc3e3222e6efd6de4 184 bytes 9158 us
# eosio <= eosio::updateauth {"account":"user","permission":"active","parent":"owner","auth":{"threshold":1,"keys":[{"key":"EOS83...
修改send.code
代码,使用active
许可发送内联action
#include
#include
#include
using namespace eosio;
class sender : public eosio::contract {
public:
using contract::contract;
void send( account_name user ) {
require_auth( user );
print( "before send inline code sender Say, ", name{user});
action(
permission_level{user, N(active)},
N(recv.code), N(receive),
user).send();
}
};
EOSIO_ABI( sender, (send) )
取消sendp
许可与send.code
的send
的关联
命令如下
~ cleos set action permission user send.code send NULL -p user@owner
executed transaction: 8ef4fbb9943adf8eecb3078a0e5214c4b127009069bd3839e0d9304243aa3d24 120 bytes 1448 us
# eosio <= eosio::unlinkauth {"account":"user","code":"send.code","type":"send"}
取消sendp
许可与recv.code
的receive
的关联
~ cleos set action permission user recv.code receive null -p user@owner
executed transaction: 9fa147d7e8d6c9fbf2c7b5c78ca3d6d2f05e327e3c10924cbbd12a4afda04723 120 bytes 2141 us
# eosio <= eosio::unlinkauth {"account":"user","code":"recv.code","type":"receive"}
执行send
action
命令如下:
~ cleos push action send.code send '["user"]' -p user@active
executed transaction: 6ecd0255f5e851ba0fc01966d64d7d7e5d2151009db5934efaebb713143490cd 104 bytes 2950 us
# send.code <= send.code::send {"user":"user"}
>> before send inline code sender Say, user
# recv.code <= recv.code::receive {"user":"user"}
>> receiver Say, user
成功了。
注意:上面我们有这两步:
- 取消
sendp
与send.code
的send
关联 - 取消
sendp
与recv.code
的receive
的关联
其实狼人杀游戏并没有这两步,因为sendp
是我们上篇文章中进行的关联,这里要先取消。如果你没有进行过这种关联,其实是不需要做这两步的。也就是说,如果你像狼人杀游戏的用法一样,一开始就使用的active
许可,那是不需要这两步取消操作的,只需要把用户的active
许可授权给合约send.code
的eosio.code
就可以了。
狼人杀的这种方式,有何安全问题?
之前狼人杀游戏需要用户把active
权限授权给它的合约的eosio.code
许可,于是玩家的账户权限信息就是这样的了:
引起了玩家以及技术圈的质疑,当时狼人杀官方对于此事的解释是这样的:
图片可能不太清楚,我把其中重点部分复述写在下面:
eosio.code的设计本意就是解决权限乱用问题,用户设置[email protected]权限表明用户授权合约调用
eosio.token
,而且授权作用域仅限合约设计时使用到的action,所以合约账号不能通过更新合约或者外部调用的方式来转走用户的EOS。BM在#3013这个issue已经提过这个问题了,官方的智能合约示例dice也实现了一个withdraw action的例子来说明这个用法。
那么是狼人杀团队正确还是质疑者正确呢?
我可以肯定的说,狼人杀团队对eosio.code
许可理解有误;上面的一段话,官方团队错误有三:
- 一旦把账号的
active
权限授权给了合约的eosio.code
,其作用域并非所谓的“合约设计时使用到的action”。而是所有原账户的active
的所有能力,都授予了合约。我们都知道active
许可权限有多大,它除了不能更改owner权限,几乎什么都能干,比如转移资产、投票、买卖ram等等。- “合约账号不能通过更新合约或者外部调用的方式来转走用户的EOS”。从问题1的解释中,也很容易知道,这句话是胡扯。后面,我们会专门演示这个问题。
- BM在#3013这个issue里,并没有说到把
active
权限授权给合约后不会有安全隐患;而是说eosio.code
机制相比之前版本中不需要用户授权就可以转移用户资产的问题而言,要安全一点。这一点我们上篇文章也做了解释。官方示例代码dice中也的确有需要用户授权active
许可的情况。其实dice
本身的设计也不太安全,EOSIO的开发者网站以前有过一篇以dice
作为范例的文章。目前已经找不到了,这个合约代码智能从eos源码中才能看到了。
其实,他们说的这些,都是在为这个观点在辩护:“active授权合约的eosio.code
后,合约无法转移用户资产,也无法转移通过更新合约来转移资产。”
下面我们就来试验一下。
如何证明狼人杀团队是错的
我们修改一下send.code
,如下:
#include
#include
#include
using namespace eosio;
class sender : public eosio::contract {
public:
using contract::contract;
void send( account_name user ) {
require_auth( user );
print( "before send inline code sender Say, ", name{user});
asset quantity(2, S(4, SYS)); // 0.0002 SYS
std::string memo = "transfer funds from user to recv.code";
action(
permission_level{N(user), N(active)},
N(eosio.token), N(transfer),
std::make_tuple(N(user), N(recv.code), quantity, memo)
).send();
}
};
EOSIO_ABI( sender, (send) )
我们从user
资产中转移了0.0002 SYS
给到recv.code
,这个数额设置的比较小,实际上可以是可以设置成任意数值的;这个代码是在我们的测试网络上试验的,如果这个代码能成功,那也就说明在主网上,狼人杀团队可以任意转移用户的EOS。
部署上述代码,同样部署在send.code
合约里(也就相当于更新了send.code
合约)。然后我们执行如下命令:
~ cleos push action send.code send '["user"]' -p send.code@active
executed transaction: 8e0ddeb37dcd4bfc680472ce8f7278a6e9c2f59c82bb36eb42ac7ce4df388261 104 bytes 10136 us
# send.code <= send.code::send {"user":"user"}
>> before send inline code sender Say, user
# eosio.token <= eosio.token::transfer {"from":"user","to":"recv.code","quantity":"0.0002 SYS","memo":"transfer funds from user to recv.cod...
# user <= eosio.token::transfer {"from":"user","to":"recv.code","quantity":"0.0002 SYS","memo":"transfer funds from user to recv.cod...
# recv.code <= eosio.token::transfer {"from":"user","to":"recv.code","quantity":"0.0002 SYS","memo":"transfer funds from user to recv.cod...
成功了!对比recv.code
和user
账户中前后SYS
的余额,你会发现,转移资产成功了。
你注意一下此命令中,我们使用的是-p send.code@active
。也就是说,根本不再需要user
的授权,send.code
合约就能转移用户的资产。实际上,上面的send
action,任何用户都可以成功执行,我们用tester
试试:
~ cleos push action send.code send '["user"]' -p tester@active
executed transaction: 14519e4785294e4a081f78c422b144a80f3d9d4605bcaf1fc0dcdbe4ed7c7c0f 104 bytes 6690 us
# send.code <= send.code::send {"user":"user"}
>> before send inline code sender Say, user
# eosio.token <= eosio.token::transfer {"from":"user","to":"recv.code","quantity":"0.0002 SYS","memo":"transfer funds from user to recv.cod...
# user <= eosio.token::transfer {"from":"user","to":"recv.code","quantity":"0.0002 SYS","memo":"transfer funds from user to recv.cod...
# recv.code <= eosio.token::transfer {"from":"user","to":"recv.code","quantity":"0.0002 SYS","memo":"transfer funds from user to recv.cod...
也能成功,为什么呢?因为user
的active权限授权给了send.code
。上面这段代码中,send.code
提供的上面的action handler
代码中,没有任何检查权限的操作;当然实际代码上,狼人杀应该不会这么"傻"。我这里只是为了演示,send.code
合约可以对user
的账户做任何事情,并且可以把这些操作,不加授权的给任何人用。
当然了,我们并不知道,狼人杀团队获取用户的active
的权限之后,在合约中都做了什么;因为他们没有开源代码。不过他们声称无法在合约中转移用户资产的说法,是完全错误的。
那么eosio.code
本身是个漏洞吗?
还是回到了原来的主题。eosio.code
是为了解决,合约在不经过用户授权的情况下可以任意使用用户账号的权限进行跨合约调用的问题而提出来的;eosio.code
只有在用户授权的情况下,才能代表用户,行使用户授予的许可的权力。这相对于之前的情况,已经在安全性上迈了一大步。用户同意授权某个合约,说明用户信任这个合约,允许它代表自己做相关操作。
你可能会问:像狼人杀游戏这般,合约代码在获得授权之后,就可以代表原用户;任意操作,这难道不也是漏洞吗?
的确,就像上面所演示的。狼人杀的合约,获得用户的active
权限之后,就可以代表用户任意操作。这是因为用户授予狼人杀合约的是active
权限。这个权限太大了,active
权限可以转移资产,把这个权限授权给了某个合约,确实太恐怖了。原因还是在于用户对于eosio.code
权限以及授权的理解不足,随随便便就把自己的权限交了出去。
在开发便利性和用户的权益之间该如何权衡呢
作为合约开发者来说,为了能在合约间通信,我们是需要用户给合约的eosio.code许可
授权的。那我们该怎么办才能尽可能保护用户的利益呢?
我们应该引导用户自定义一个许可。就像上一篇文章中,我们做的那样,把这个自定义的许可授权给我们的合约。
回忆一下上篇文章,我们自定义了一个sendp
许可,授权给了send.code
合约,并且sendp
关联了send.code
合约的send
action,以及recv.code
合约的receive
action。
合约使用自定义许可,只能发送用户已经关联的action
;如果没有关联eosio.token
的transfer
的action,合约将不能够转移资产。并且用户在授权后,可随时取消关联;或者删除这个自定义的权限,这样之前被授权的合约也就无法做相关的动作。
这样,用户就能够对自己的账户有更好的掌控力,可以控制合约使用许可的能力。
然而,自定义许可,是一种相对复杂的机制。目前很多的用户都无法理解,需要一段时间的市场教育。
所以我的建议是,在目前用户还不熟悉EOS的权限的情况下,尽可能不要跨合约调用。如果需要使用inline action
,可以把两个合约合二为一,在合约内部调用。合约内部调用是不需要eosio.code
的,就像EOS开发入门系列(16)
中所演示的那样。当然了,如果非要使用跨合约调用,还是尽可能的使用自定义许可。
简介:不羁,一名程序员;专研EOS技术,玩转EOS智能合约开发。
微信公众号:know_it_well
知识星球地址:https://t.zsxq.com/QvbuzFM