move 漏洞类型

2025-06-25

1. Move语言常见漏洞

简单介绍一下move的常见漏洞:(接下来详细介绍的问题其实都属于这些基本漏洞中的一种或多种)

1.1 未验证输入

有些输入,在使用前未经过验证,比如允许你设置为2^64-1,那么将导致匪夷所思的问题:

public entry fun check_access(score: u64) {
    if (score+1 > 50) { 
        return false;
    }else {
        return true;
    }
}

虽然例子有些离谱,但是确实是这样的。

1.2 逻辑错误

有的开发者在开发时,会遗漏临界条件,如:

public struct AccessControl has key {
    id: UID,
    is_allowed: bool,
    threshold: u64
}

public entry fun check_access(access: &mut AccessControl, score: u64) {
    if (score > 50) { // 应为 >= 50,但是设置为 >50
        access.is_allowed = true;
    }
}

当然更复杂的比如逻辑写反了等等,也是比较常见的。

public entry fun vote(
    store: &mut VoteStore,
    vote_coin: Coin<VOTE>,
    proposal_name: String,
    ctx: &mut TxContext
) {
    assert!(vote_coin.value() > 0, E_INVALID_AMOUNT);
    assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL);
    let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name);
    let sender = tx_context::sender(ctx);
    let amount = coin::into_balance(vote_coin);
    if (vec_map::contains(&store.voters, &sender)) {
        let voter_amount = vec_map::get_mut(&mut store.voters, &sender);
        *voter_amount = *voter_amount + amount.value();
    } else {
        vec_map::insert(&mut store.voters, sender, amount.value());
    };
    proposal.votes = proposal.votes + amount.value();
    proposal.locked_tokens.join(amount);
}

public entry fun close_proposal(
    store: &mut VoteStore,
    proposal_name: String,
    ctx: &mut TxContext
) {
    assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL);
    let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name);
    let sender = tx_context::sender(ctx);
    assert!(sender == proposal.owner, E_UNAUTHORIZED);
    assert!(!proposal.closed, E_PROPOSAL_CLOSED);
    proposal.closed = true;
    let coin = coin::from_balance(proposal.locked_tokens.withdraw_all(), ctx);
    public_transfer(coin, sender);
}

这个问题中,在close_proposal后,仍然可以进行投票!!!

1.2.1 逻辑漏洞防御

检验状态一致性非常重要,上述例子中,投票前需要校验proposal是否已经被关闭。

1.3 权限控制不足

函数未限制调用者身份,允许任何人执行敏感操作。

Move 的 public entry fun 默认对所有地址开放,需手动验证 tx_context::sender.
Sui 的共享对象尤其需注意权限。
比如:
public entry fun reset_counter(counter: &mut Counter) { // 未验证调用者
  counter.count = 0; 
}

导致任何人都可以重置计数器。
还有一个例子:

public entry fun close_proposal(
        store: &mut VoteStore,
        proposal_name: String,
        ctx: &mut TxContext
    ) {
        assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL);
        let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name);
        assert!(!proposal.closed, E_PROPOSAL_CLOSED);
        proposal.closed = true;
        let coin = from_balance(proposal.locked_tokens.withdraw_all(), ctx);
        public_transfer(coin, tx_context::sender(ctx));
    }

在这个代码中没有对proposal进行管控,导致任何人都可以关闭proposal

1.3.1 权限控制漏洞防御

在一些敏感的功能中,需要添加权限访问机制,例如assert!(proposal.owner == tx_context::sender(ctx), E_UNAUTHORIZED);来校验使用敏感功能的身份。或者直接一点,直接不公开敏感操作即可。

1.4 整数溢出/下溢

在 Sui 中,Move 的整数运算(如 u64 的加法、减法)默认启用溢出检查,溢出或下溢会导致交易失败.

module counter::counter{
    use sui::event;

    public struct Counter has key {
        id: UID,
        count: u64,
    }

    public struct CounterEmit has copy, drop{
        count: u64,
    }

    fun init(ctx: &mut TxContext){
        transfer::share_object(Counter { id:object::new(ctx), count: 0 });
    }

    public entry fun add(counter: &mut Counter,amount: u64){
        counter.count = counter.count + amount;
        event::emit(CounterEmit { count: counter.count })
    }

    public entry fun reduce(counter: &mut Counter,amount: u64){
        counter.count = counter.count - amount;
        event::emit(CounterEmit { count: counter.count })
    }
}

1.5 资源管理不当

资源未正确转移或销毁,导致编译错误。这个属于编译错误,对安全性的影响倒不大。(对开发影响很大,非常影响体验)
当然还有其他资源管理不当的例子:

public entry fun vote(
    store: &mut VoteStore,
    vote_coin: &Coin<VOTE>,
    proposal_name: String,
    ctx: &mut TxContext
) {
    assert!(vote_coin.value() > 0, E_INVALID_AMOUNT);
    assert!(object_table::contains(&store.proposals, proposal_name), E_INVALID_PROPOSAL);
    let sender = tx_context::sender(ctx);
    if (!vec_map::contains(&store.voters, &sender)) {
        vec_map::insert(&mut store.voters, sender, true);
    };
    let proposal = object_table::borrow_mut(&mut store.proposals, proposal_name);
    proposal.votes = proposal.votes + vote_coin.value();
}

这段代码中,对于已经投过票的coin对象,没有进行销毁,导致用户可以靠一个coin对象进行对此投票。

1.5.1 资源管理漏洞防御方法

1、已经投过的coin对象,要及时销毁
2、验证状态,验证coin对象是否是有效的
3、最小化共享对象,share_object中的敏感数据要尽可能少(不然会被操纵)

2. 泛型类型安全

Sui Move中,泛型类型是由调用者在运行时提供的用户输入。如果合约未验证泛型类型<T>是否符合预期,攻击者可以传入任意类型,导致难以预想的问题。可以判断,其实泛型类型漏洞属于未验证输入的一种
一个典型的具有泛型参数的函数的例子如下:

public entry fun register_voter<T>(vote_coin: coin::Coin<T>, ctx: &mut TxContext) {
    let amount = vote_coin.value();
    assert!(amount == 100,1);
    let sender = tx_context::sender(ctx);
    let token = VoteToken<T> {
        id: object::new(ctx),
        amount: 100,
    };
    public_transfer(token, sender);
    public_transfer(vote_coin, @0x0);
}

其中T是我们可控的参数,如果不进行限制,将导致:

伪造凭证:攻击者创建非法类型的对象(如 VoteToken<FakeToken>)绕过权限检查。
逻辑破坏:非预期类型导致合约状态异常,影响核心功能(如投票结果错误)。
资源滥用:攻击者利用伪造类型创建无效资源,干扰合约运行或耗尽 Gas.

2.1 泛型漏洞的抵御方法

最直接的抵御方法,是在使用前,进行校验:

use std::type_name;//std::type_name能够检测传入的泛型的完整类型名称(模块名::类型名称)

public entry fun register_voter<T>(ctx: &mut TxContext) {
    assert!(type_name::get<T>() == type_name::get<votechain::OfficialToken>(), 3);
    let sender = tx_context::sender(ctx);
    let token = VoteToken<T> {
        id: object::new(ctx),
        amount: 100,
    };
    public_transfer(token, sender);
}

另外就是在开发时要注意不要有事没事就用泛型,使用泛型的话要注意显示的检查(白名单)就好了。

3. 跨合约交互安全

公开到链上的合约,能够被其他合约调用,详见:https://learnblockchain.cn/article/7623