[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 入账。
复现步骤
- 使用当前
openmetaearth/me-hub 测试网源码或等价本地开发环境。
- 给
experience region treasury 注入 100000umec。
- 取
fee_collector 模块账户地址,并确认 BankKeeper.BlockedAddr(fee_collector) == true。
- 以 GlobalDao 调用
MsgWithdrawFromRegion,Receiver = fee_collector,Amount = 100000umec。
- 预期:交易应拒绝 blocked module account 作为普通提款接收方,且
fee_collector 余额保持 0。
- 实际:handler 返回
nil,fee_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
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:
- A GlobalDao transaction or a granted region withdrawer can transfer region treasury funds into a blocked module account such as
fee_collector.
- The transfer succeeds through normal keeper logic and emits a normal bank transfer event.
- The blocked module account receives ordinary user/treasury funds outside its expected module-specific accounting path.
- 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.
[BUG BOUNTY] [Medium] [WStaking/WBank] Region treasury withdrawal can fund blocked module accounts
漏洞标题
Region treasury withdrawal can fund blocked module accounts
受影响模块
wstakingregion treasury withdrawalwbanktagged transfer helper漏洞描述
MsgWithdrawFromRegion允许 GlobalDao 或被授权地址从国家区金库提现到任意Receiver。该路径只校验Receiver是合法 Bech32 地址,然后调用:SendCoinsWithTag内部直接调用BaseKeeper.SendCoins,不会像SendCoinsFromModuleToAccountWithTag那样检查BlockedAddr(toAddr)。因此,区域金库资金可以被转入fee_collector等 blocked module account。这绕过了应用层把模块账户标记为 blocked 的资金保护边界。资金一旦被普通提款路径打入受保护模块账户,可能永久锁定或污染模块账户余额,且没有通过该模块预期的 keeper API 入账。
复现步骤
openmetaearth/me-hub测试网源码或等价本地开发环境。experienceregion treasury 注入100000umec。fee_collector模块账户地址,并确认BankKeeper.BlockedAddr(fee_collector) == true。MsgWithdrawFromRegion,Receiver = fee_collector,Amount = 100000umec。fee_collector余额保持 0。nil,fee_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:
PoC(中危及以上必填)
本地红灯测试:
x/wstaking/keeper/msg_server_region_test.goTestKeeperTestSuite/TestWithdrawFromRegionRejectsBlockedModuleReceiver关键断言:
环境信息
mechain_900-1http://118.175.0.244:26657/status35907622CFC81436FBE58C9BB83988A4957D11CDBB55907BDF270B07C75EB2BBA634BDF8cee4e10c8843cd1f0ad631053da3c48eff80961yuzhiyang1me1p8u5377smm8zkfq9dmcg6prwflap0ndj4ht34z1585004297@qq.com/@y_zhiyRoot Cause
Affected code:
x/wstaking/keeper/msg_server_region.goWithdrawFromRegionx/wbank/keeper/keeper.goBankKeeperExtend.SendCoinsWithTagWithdrawFromRegionaccepts any syntactically valid receiver:It then sends region treasury funds through the generic tagged transfer helper:
SendCoinsWithTagonly adds event tags after a successfulSendCoinscall:It does not reject blocked module accounts. This is inconsistent with
SendCoinsFromModuleToAccountWithTag, which explicitly protects blocked receivers:Impact
This affects real funds held by regional treasuries:
fee_collector.This is related to, but distinct from, #298 / #958:
app/antefee receivers andFeeToReceivers.wstaking.MsgWithdrawFromRegion, a direct region treasury withdrawal path with a user-supplied receiver and a different fund source.Recommendation
Reject blocked module account receivers before moving funds:
Or add the guard centrally in
SendCoinsWithTagfor account-to-account transfers that represent ordinary user/business withdrawals. Add regression coverage for:fee_collector;