保管好私钥就安全了吗?注意隐藏在EOS DAPP中的安全隐患

保管好私钥就安全了吗?注意隐藏在EOS DAPP中的安全隐患

概述

EOS 的 DAPP 开发过程中,有些 DAPP 业务功能需要与账户转账关联起来,业务逻辑要求用户给合约转账才能实现。 但是我们发现有些 DAPP 开发者对 EOS 的相关机制不太了解, 导致一些安全问题。 本文站在开发人员的角度, 分析了实现这种业务逻辑的几种方法。


方法一:授权合约

这类 DAPP 会要求用户授权后才能继续, 这个风险最大, 用户应该立即马上停止使用。 原理如下:
用户 A 使用合约 C 之前, 必须通过updateauth 授权给合约, 这个一般是这样实现的。

void komo::buykey(account_name from, asset quantity) 
{
  require_auth(from);
  // inline action requires updateauth with eosio.code
  action act(
      permission_level{from, N(active)},
      N(eosio.token), N(transfer),
      std::make_tuple(from, _self, quantity, std::string("")));
  act.send();

  biz_buy_key(from, quantity);
}

在合约内发起转账需要用户授权给合约账户的 eosio.code 。
下面的例子是用户 aaaaaaaaaaaa 授权给合约 cccccccccccc , 方法如下:

cleos set account permission aaaaaaaaaaaa active '{"threshold": 1,"keys": [{"key": "aaaaaaaaaaaa_public_key","weight": 1}],"accounts": [{"permission":{"actor":"cccccccccccc","permission":"eosio.code"},"weight":1}]}' owner -p aaaaaaaaaaaa

EOS 在执行 inline actions 时会检查合约账户的 eosio.code 权限:

void apply_context::execute_inline( action&& a ) {
   auto* code = control.db().find<account_object, by_name>(a.account);
   EOS_ASSERT( code != nullptr, action_validate_exception,
               "inline action's code account ${account} does not exist", ("account", a.account) );

   for( const auto& auth : a.authorization ) {
      auto* actor = control.db().find<account_object, by_name>(auth.actor);
      EOS_ASSERT( actor != nullptr, action_validate_exception,
                  "inline action's authorizing actor ${account} does not exist", ("account", auth.actor) );
      EOS_ASSERT( control.get_authorization_manager().find_permission(auth) != nullptr, action_validate_exception,
                  "inline action's authorizations include a non-existent permission: ${permission}",
                  ("permission", auth) );
   }

   // No need to check authorization if: replaying irreversible blocks; contract is privileged; or, contract is calling itself.
   if( !control.skip_auth_check() && !privileged && a.account != receiver ) {
      control.get_authorization_manager()
             .check_authorization( {a},
                                   {},
                                   {{receiver, config::eosio_code_name}},
                                   control.pending_block_time() - trx_context.published,
                                   std::bind(&transaction_context::checktime, &this->trx_context),
                                   false
                                 );

      //QUESTION: Is it smart to allow a deferred transaction that has been delayed for some time to get away
      //          with sending an inline action that requires a delay even though the decision to send that inline
      //          action was made at the moment the deferred transaction was executed with potentially no forewarning?
   }

   _inline_actions.emplace_back( move(a) );
}

为什么这个最危险呢, 因为一旦用户授权给了合约的 eosio.code , 这个合约就有了用户的active 权限,可以做很多事了, 比如把用户的EOS全部转走。
强烈建议马上停止使用此类 DAPP , 并且检查自己的授权是否包含 eosio.code 。

$ ./x_cleos.sh get account your_account
permissions:     owner     1:    1 EOS61ErKWxHQF6AoSKRc7GJb2HmorQpsC6uciQq1kDiPcZVfHZAU5
        active     1:    1 EOS5eiF9mFxVqYG8Mjv9A2uE4ZDFAdqDQtrvgV3yWBGsiNz8LzZ5X1 test22222222@eosio.code,


方法二:使用通知

这种方式利用了 eosio.token::transfer 方法的 require_recipient , 不需要授权给合约, 只需要给合约账户转账即可,相对比较安全。

void token::transfer( account_name from,
                      account_name to,
                      asset        quantity,
                      string       memo )
{
    eosio_assert( from != to, "cannot transfer to self" );
    require_auth( from );
    eosio_assert( is_account( to ), "to account does not exist");
    auto sym = quantity.symbol.name();
    stats statstable( _self, sym );
    const auto& st = statstable.get( sym );

    require_recipient( from );
    require_recipient( to );

    eosio_assert( quantity.is_valid(), "invalid quantity" );
    eosio_assert( quantity.amount > 0, "must transfer positive quantity" );
    eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );


    sub_balance( from, quantity );
    add_balance( to, quantity, from );
}

实现原理是这样的, 用户给合约账户转账时会触发 require_recipient 调用, 合约开发者可以接收这个通知, 完成自己的业务, 举例如下:

void komo::transfer(account_name from, account_name to, asset quantity, std::string memo)
{
  if (from == _self || to != _self) {
    return;
  }

  biz_buy_key(from, quantity);
  return;
}

apply 中允许 eosio.token::transfer 触发调用。

\#define EOSIO_ABI_EX( TYPE, MEMBERS ) \
extern "C" { \
    void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
        auto self = receiver; \
        if( action == N(onerror)) { \
            /* onerror is only valid if it is for the "eosio" code account and authorized by "eosio"'s "active permission */ \
            eosio_assert(code == N(eosio), "onerror action's are only valid from the \"eosio\" system account"); \
        } \
        if ((code == self && action != N(transfer)) || (code == N(eosio.token) && action == N(transfer)) ) { \
          TYPE thiscontract( self ); \
            switch( action ) { \
                EOSIO_API( TYPE, MEMBERS ) \
            } \
         /* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
        } \
    } \
} \

EOSIO_ABI_EX(komo, (transfer))


方法三: 组合交易

既然要求用户转账和业务逻辑绑定在一起, 那为什么不可以在客户端发起交易时就把这2个action 包在一个交易里呢? 实现稍微复杂一些, 下面改造了一下 cleos:

struct komo_subcommand {
    string contract;
    string player;
    string buy_key_amount;

    komo_subcommand(CLI::App* actionRoot) {
      auto komo = actionRoot->add_subcommand("komo", localized("play komo"));
      komo->add_option("contract", contract, localized("The contract"))->required();
      komo->add_option("player", player, localized("The account to play komo"))->required();
      komo->add_option("buy_key_amount", buy_key_amount, localized("The amount of EOS to pay for keys"))->required();
      add_standard_transaction_options(komo);

      komo->set_callback([this] {
         auto transfer = create_transfer("eosio.token", player, contract, to_asset(buy_key_amount), ""); 
         fc::variant act_payload = fc::mutable_variant_object()
                  ("from", player)
                  ("quantity", to_asset(buy_key_amount));
         auto buykey = create_action({permission_level{player, config::active_name}}, contract, N(buykey), act_payload); 
         send_actions({transfer, buykey});
      });
   }
};

auto buykey = komo_subcommand(&app);

这样2个action就放到一个交易里。
在合约代码里检测转账交易:

void komo::buykey(account_name from, asset quantity) 
{
  require_auth(from);
  // Check the first action
  auto act = get_action(1, 0);
  
  eosio_assert( act.authorization.back().actor == from, "incorrect permission actor" );
  eosio_assert( act.authorization.back().permission == N(active), "incorrect permission name" );
  eosio_assert(act.account == N(eosio.token) && act.name == N(transfer), "first action should be the transfer action!");

  auto transfer = act.data_as<transfer_args>();
  eosio_assert(transfer.from == from, "should be the same user");
  eosio_assert(transfer.to == _self, "receipt should be the contract");
  eosio_assert(transfer.quantity == quant, "quantity mismatched");
  eosio_assert(transfer.quantity.symbol == CORE_SYMBOL, "invalid symbol");
  // ...
}

这种方式既不需要授权,也不需要依赖系统合约的通知, 但是transfer action会被同一交易里的其他多个action 使用, 会导致一次transfer被多次确认的问题, 除非限制这个交易只有2个action。

总结

EOS 目前的合约机制还不完善,希望大家在使用第三方 DAPP 时一定要注意风险, 避免损失。

如果您对美图技术感兴趣,请关注美图技术团队机构号;如果您对美图感兴趣请留言;如果您觉得作者内容对您有帮助,请随手点个赞。

美图技术团队 - 知乎www.zhihu.com图标

编辑于 2018-09-21