Skip to content

[BUG BOUNTY] [Medium] [WStaking/WBank] Region treasury withdrawal can fund blocked module accounts #1081

Description

@yuzhiyang1

[BUG BOUNTY] [Medium] [WStaking/WBank] Region treasury withdrawal can fund blocked module accounts

漏洞标题

Region treasury withdrawal can fund blocked module accounts

受影响模块

  • ME-Hub
  • wstaking region treasury withdrawal
  • wbank tagged transfer helper
  • 官方 Phase 1 范围映射:wstaking 质押业务 / Gas 费与资金流保护相关机制
  • 建议等级:Medium / 中

漏洞描述

MsgWithdrawFromRegion 允许 GlobalDao 或被授权地址从国家区金库提现到任意 Receiver。该路径只校验 Receiver 是合法 Bech32 地址,然后调用:

k.bankKeeper.Extend().SendCoinsWithTag(ctx, fromAddr, toAddr, msg.Amount, ...)

SendCoinsWithTag 内部直接调用 BaseKeeper.SendCoins,不会像 SendCoinsFromModuleToAccountWithTag 那样检查 BlockedAddr(toAddr)。因此,区域金库资金可以被转入 fee_collector 等 blocked module account。

这绕过了应用层把模块账户标记为 blocked 的资金保护边界。资金一旦被普通提款路径打入受保护模块账户,可能永久锁定或污染模块账户余额,且没有通过该模块预期的 keeper API 入账。

复现步骤

  1. 使用当前 openmetaearth/me-hub 测试网源码或等价本地开发环境。
  2. experience region treasury 注入 100000umec
  3. fee_collector 模块账户地址,并确认 BankKeeper.BlockedAddr(fee_collector) == true
  4. 以 GlobalDao 调用 MsgWithdrawFromRegionReceiver = fee_collectorAmount = 100000umec
  5. 预期:交易应拒绝 blocked module account 作为普通提款接收方,且 fee_collector 余额保持 0。
  6. 实际:handler 返回 nilfee_collector 收到 100000umec

PoC 命令:

wsl.exe -- bash -lc "cd /mnt/d/Backup/Documents/Playground/me-hub && CGO_ENABLED=1 CC='/mnt/d/Backup/Documents/Playground/.tools/wsl/zig-x86_64-linux-0.16.0/zig cc' GOPROXY=https://goproxy.cn,https://proxy.golang.org,direct GOSUMDB=sum.golang.google.cn /mnt/d/Backup/Documents/Playground/.tools/wsl/go/bin/go test ./x/wstaking/keeper -run 'TestKeeperTestSuite/TestWithdrawFromRegionRejectsBlockedModuleReceiver' -count=1 -v"

Observed output:

=== RUN   TestKeeperTestSuite
=== RUN   TestKeeperTestSuite/TestWithdrawFromRegionRejectsBlockedModuleReceiver
    msg_server_region_test.go:265: blocked receiver=me17xpfvakm2amg962yls6f84z3kell8c5lr2wff2 err=<nil> balance_after=100000umec
    msg_server_region_test.go:266:
        	Error:      	An error is expected but got nil.
        	Messages:   	blocked module account must not receive ordinary region treasury withdrawals
    msg_server_region_test.go:267:
        	Error:      	Should be true
        	Messages:   	blocked module account must not be funded by region treasury withdrawal
--- FAIL: TestKeeperTestSuite (0.07s)
    --- FAIL: TestKeeperTestSuite/TestWithdrawFromRegionRejectsBlockedModuleReceiver (0.07s)
FAIL
FAIL	github.com/openmetaearth/me-hub/x/wstaking/keeper	0.160s

PoC(中危及以上必填)

本地红灯测试:

  • x/wstaking/keeper/msg_server_region_test.go
  • Test: TestKeeperTestSuite/TestWithdrawFromRegionRejectsBlockedModuleReceiver

关键断言:

blockedReceiver := authtypes.NewModuleAddress(authtypes.FeeCollectorName)
s.Require().True(s.App.BankKeeper.BlockedAddr(blockedReceiver))

_, err := s.msgServer.WithdrawFromRegion(s.Ctx, &types.MsgWithdrawFromRegion{
	Withdrawer: s.Dao.GlobalDao,
	RegionId:   types.ExperienceRegionId,
	Receiver:   blockedReceiver.String(),
	Amount:     coins,
})

s.Assert().Error(err)
s.Assert().True(s.App.BankKeeper.GetBalance(s.Ctx, blockedReceiver, params.BaseDenom).IsZero())

环境信息

  • Chain ID: mechain_900-1
  • RPC: http://118.175.0.244:26657/status
  • 测试网区块高度: 3590762
  • 测试网 AppHash: 2CFC81436FBE58C9BB83988A4957D11CDBB55907BDF270B07C75EB2BBA634BDF
  • Code commit under review: 8cee4e10c8843cd1f0ad631053da3c48eff80961
  • Tx Hash: N/A, deterministic local keeper PoC; no public testnet fund-moving transaction was broadcast.
  • 是否可稳定复现: Yes
  • Reporter GitHub: yuzhiyang1
  • MEC Address: me1p8u5377smm8zkfq9dmcg6prwflap0ndj4ht34z
  • Contact: 1585004297@qq.com / @y_zhiy
  • 安全边界:未测试主网,未攻击第三方服务,未使用社工方式。

Root Cause

Affected code:

  • x/wstaking/keeper/msg_server_region.go
    • WithdrawFromRegion
  • x/wbank/keeper/keeper.go
    • BankKeeperExtend.SendCoinsWithTag

WithdrawFromRegion accepts any syntactically valid receiver:

toAddr, err := sdk.AccAddressFromBech32(msg.Receiver)
if err != nil {
	return nil, sdkerrors.Wrapf(types.ErrUnknownAccount, "receiver account %s format error %s", msg.Receiver, err)
}

It then sends region treasury funds through the generic tagged transfer helper:

err = k.bankKeeper.Extend().SendCoinsWithTag(ctx, fromAddr, toAddr, msg.Amount, ...)

SendCoinsWithTag only adds event tags after a successful SendCoins call:

err := k.SendCoins(ctx, fromAddr, toAddr, amt)
if err != nil {
	return err
}

It does not reject blocked module accounts. This is inconsistent with SendCoinsFromModuleToAccountWithTag, which explicitly protects blocked receivers:

if k.BlockedAddr(recipientAddr) {
	return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", recipientAddr)
}

Impact

This affects real funds held by regional treasuries:

  1. A GlobalDao transaction or a granted region withdrawer can transfer region treasury funds into a blocked module account such as fee_collector.
  2. The transfer succeeds through normal keeper logic and emits a normal bank transfer event.
  3. The blocked module account receives ordinary user/treasury funds outside its expected module-specific accounting path.
  4. Funds can become stuck or mixed into module balances with no normal user key able to recover them.

This is related to, but distinct from, #298 / #958:

Recommendation

Reject blocked module account receivers before moving funds:

if k.bankKeeper.BlockedAddr(toAddr) {
	return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive region treasury withdrawals", toAddr)
}

Or add the guard centrally in SendCoinsWithTag for account-to-account transfers that represent ordinary user/business withdrawals. Add regression coverage for:

  • region treasury withdrawal to fee_collector;
  • region treasury withdrawal to a normal account;
  • granted withdrawer attempting a blocked receiver.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions