智能合约审计系列————2、权限隐患&条件竞争
字数 2388 2025-08-22 12:22:30

智能合约审计系列:权限隐患与条件竞争漏洞详解

0x00 前言

本文是智能合约审计系列的第二篇,重点探讨智能合约开发中常见的权限隐患和条件竞争问题。这些安全问题往往由于开发者疏忽或设计不当导致,可能造成严重的安全后果。

0x01 基础知识

权限的概念

  • 权限定义:指对某事项进行决策的范围和程度
  • 智能合约用户分类
    • 合约owner:拥有合约全部权限,可执行所有函数
    • 普通用户:只能执行约定范围内的函数,无法执行权限校验函数(如onlyOwner修饰的函数)

Solidity函数可见性

修饰符 可调用范围
public 合约内部函数、继承合约、外部合约均可调用(默认可见性)
private 仅限合约内部函数调用(继承合约调用会编译报错)
internal 合约内部函数及继承合约可调用
external 仅限外部合约调用

0x02 权限隐患

构造函数权限问题

构造函数的作用

用于初始化合约对象的状态变量,具有以下特点:

  • 函数名与合约名相同(0.4.22版本前)
  • 可用public或internal修饰
  • 若有payable修饰,则必须是public类型
  • 若带参数,必须放在合约第一个函数位置

版本变化

  • 0.4.22版本前:构造函数名必须与合约名相同
  • 0.4.22版本后:引入constructor关键字替代旧语法

常见安全问题

  1. 构造函数名与合约名不一致

    • 旧版本中,构造函数名若与合约名不一致(如大小写错误、拼写错误),该函数会变为普通public函数
    • 示例:ReaperCoin11合约中reaper11函数本应是构造函数,但因名称不匹配成为公开函数
  2. constructor使用不规范

    • 新版本中错误使用function constructor()function Constructor()会使构造函数变为普通函数
    • 示例:MDOT合约和TOGToken合约中的不规范写法

安全建议:使用规范的构造函数定义方法,新版本统一使用constructor关键字

普通函数权限问题

常见漏洞模式

  1. 特权函数滥用

    • 示例1:Token合约中的burn函数
      • 使用onlyAuthorized修饰器(实际为onlyOwner)
      • owner可销毁任意用户的代币(包括归零)
    • 示例2:sacToken合约中的melt函数
      • 使用onlyCFO修饰器(实际为onlyOwner)
      • owner可销毁任意用户的代币
  2. 后门函数

    • 开发者预留的未公开特权函数
    • 通常使用隐蔽的修饰器或权限检查

安全建议

  • 严格把控函数逻辑流程
  • 合理设置参数定义
  • 谨慎使用修饰器和可见性修饰词
  • 避免预留不必要的特权函数

0x03 条件竞争

基本概念

条件竞争(竞态条件)指由于并发操作处理不当导致的安全问题。在以太坊中表现为:

  • 区块链交易公开可见
  • 恶意用户可观察未决交易并抢先执行
  • 典型场景:ERC20的approve-transferFrom流程

ERC20中的条件竞争问题

标准approve函数问题

function approve(address _spender, uint256 _value) public returns (bool)

攻击场景

  1. 用户A批准用户B转账额度N
  2. 一段时间后,用户A欲将额度改为M,再次调用approve
  3. 用户B在第二次approve被处理前,迅速用transferFrom转走N
  4. 第二次approve生效后,用户B又获得M额度
  5. 最终用户B拥有N+M的转账额度

解决方案讨论

  1. Ethereum官方建议

    • 修改额度时必须先设为0再设新值
    • 代码实现:
      require((_value == 0) || (allowed[msg.sender][_spender] == 0));
      
    • 问题:违反ERC20标准规范
  2. OpenZeppelin方案

    • 使用increaseApprovaldecreaseApproval替代直接approve
    • 但仍保留标准approve函数(无require检查)
    • 问题:若用户仍调用approve而非新增函数,问题依旧存在
  3. 实际合约中的三种处理方式

    • 类型1:在approve中增加require检查(安全但非标准)
    • 类型2:使用increase/decreaseApproval,保持标准approve(仍不安全)
    • 类型3:结合前两种,既用新函数又在approve中加检查(最安全但最不标准)

安全与标准的权衡

  • 安全角度:类型1和类型3更安全
  • 标准角度:类型2符合ERC20标准

0x04 总结与最佳实践

开发建议

  1. 建立严格的开发规范

    • 函数命名、权限控制、可见性使用等
    • 特别是构造函数的使用规范
  2. 持续学习安全知识

    • 跟踪公开的合约漏洞案例
    • 了解攻击手法和防御方案
  3. 权限控制原则

    • 最小权限原则
    • 避免不必要的特权函数
    • 关键操作设置多重验证
  4. 条件竞争防御

    • 根据项目需求在安全与标准间权衡
    • 考虑使用增量式授权而非直接覆盖
    • 重要操作添加状态检查
  5. 专业审计

    • 上线前进行专业安全审计
    • 包括静态分析和动态测试

经典格言

"道路千万条,安全第一条。编码不规范,亲人两行泪"

通过规范开发流程、严格权限控制和正确处理并发问题,可显著提高智能合约的安全性,避免重大损失。

智能合约审计系列:权限隐患与条件竞争漏洞详解 0x00 前言 本文是智能合约审计系列的第二篇,重点探讨智能合约开发中常见的权限隐患和条件竞争问题。这些安全问题往往由于开发者疏忽或设计不当导致,可能造成严重的安全后果。 0x01 基础知识 权限的概念 权限定义 :指对某事项进行决策的范围和程度 智能合约用户分类 : 合约owner :拥有合约全部权限,可执行所有函数 普通用户 :只能执行约定范围内的函数,无法执行权限校验函数(如onlyOwner修饰的函数) Solidity函数可见性 | 修饰符 | 可调用范围 | |------------|--------------------------------------------------------------------------| | public | 合约内部函数、继承合约、外部合约均可调用(默认可见性) | | private | 仅限合约内部函数调用(继承合约调用会编译报错) | | internal | 合约内部函数及继承合约可调用 | | external | 仅限外部合约调用 | 0x02 权限隐患 构造函数权限问题 构造函数的作用 用于初始化合约对象的状态变量,具有以下特点: 函数名与合约名相同(0.4.22版本前) 可用public或internal修饰 若有payable修饰,则必须是public类型 若带参数,必须放在合约第一个函数位置 版本变化 0.4.22版本前 :构造函数名必须与合约名相同 0.4.22版本后 :引入 constructor 关键字替代旧语法 常见安全问题 构造函数名与合约名不一致 旧版本中,构造函数名若与合约名不一致(如大小写错误、拼写错误),该函数会变为普通public函数 示例:ReaperCoin11合约中 reaper11 函数本应是构造函数,但因名称不匹配成为公开函数 constructor使用不规范 新版本中错误使用 function constructor() 或 function Constructor() 会使构造函数变为普通函数 示例:MDOT合约和TOGToken合约中的不规范写法 安全建议 :使用规范的构造函数定义方法,新版本统一使用 constructor 关键字 普通函数权限问题 常见漏洞模式 特权函数滥用 示例1:Token合约中的 burn 函数 使用 onlyAuthorized 修饰器(实际为onlyOwner) owner可销毁任意用户的代币(包括归零) 示例2:sacToken合约中的 melt 函数 使用 onlyCFO 修饰器(实际为onlyOwner) owner可销毁任意用户的代币 后门函数 开发者预留的未公开特权函数 通常使用隐蔽的修饰器或权限检查 安全建议 : 严格把控函数逻辑流程 合理设置参数定义 谨慎使用修饰器和可见性修饰词 避免预留不必要的特权函数 0x03 条件竞争 基本概念 条件竞争(竞态条件)指由于并发操作处理不当导致的安全问题。在以太坊中表现为: 区块链交易公开可见 恶意用户可观察未决交易并抢先执行 典型场景:ERC20的approve-transferFrom流程 ERC20中的条件竞争问题 标准approve函数问题 攻击场景 : 用户A批准用户B转账额度N 一段时间后,用户A欲将额度改为M,再次调用approve 用户B在第二次approve被处理前,迅速用transferFrom转走N 第二次approve生效后,用户B又获得M额度 最终用户B拥有N+M的转账额度 解决方案讨论 Ethereum官方建议 : 修改额度时必须先设为0再设新值 代码实现: 问题 :违反ERC20标准规范 OpenZeppelin方案 : 使用 increaseApproval 和 decreaseApproval 替代直接approve 但仍保留标准approve函数(无require检查) 问题 :若用户仍调用approve而非新增函数,问题依旧存在 实际合约中的三种处理方式 : 类型1 :在approve中增加require检查(安全但非标准) 类型2 :使用increase/decreaseApproval,保持标准approve(仍不安全) 类型3 :结合前两种,既用新函数又在approve中加检查(最安全但最不标准) 安全与标准的权衡 : 安全角度:类型1和类型3更安全 标准角度:类型2符合ERC20标准 0x04 总结与最佳实践 开发建议 建立严格的开发规范 函数命名、权限控制、可见性使用等 特别是构造函数的使用规范 持续学习安全知识 跟踪公开的合约漏洞案例 了解攻击手法和防御方案 权限控制原则 最小权限原则 避免不必要的特权函数 关键操作设置多重验证 条件竞争防御 根据项目需求在安全与标准间权衡 考虑使用增量式授权而非直接覆盖 重要操作添加状态检查 专业审计 上线前进行专业安全审计 包括静态分析和动态测试 经典格言 "道路千万条,安全第一条。编码不规范,亲人两行泪" 通过规范开发流程、严格权限控制和正确处理并发问题,可显著提高智能合约的安全性,避免重大损失。