10分钟智能合约:进阶实战-3.7 初始化失败漏洞

65 阅读6分钟

欢迎订阅专栏10分钟智能合约:进阶实战

初始化失败漏洞:定义、原理与防御

初始化失败漏洞 指智能合约在部署或启动时,关键状态(如 owner、合约版本、依赖地址)未能正确、完整地初始化,导致合约处于不安全的“未初始化”或“部分初始化”状态。攻击者可利用该缺陷抢先调用初始化函数获得合约控制权,或绕过权限检查造成资金损失。

该漏洞在可升级合约(代理模式)中尤为常见,因构造函数在此模式下无效,必须使用单独的 initialize 函数。


1. 核心原理

传统合约通过 构造函数constructor)在部署时自动执行初始化。构造函数有以下特点:

  • 仅在部署时调用一次,之后无法再执行。
  • 在代理模式中,逻辑合约的构造函数不会在代理的上下文中执行(因为 delegatecall 不执行构造函数)。

因此,可升级合约普遍采用 初始化函数(如 initialize)代替构造函数。若该函数:

  • 缺少仅调用一次的保护(initializer 修饰符);
  • 未正确调用;
  • 调用后状态设置不完整;

则合约可能被攻击者抢先初始化(Front-Running Initialization),或永远处于未初始化状态,从而被恶意接管。


2. 常见漏洞场景

场景描述后果
代理合约中未调用 initialize逻辑合约部署后,管理员忘记调用初始化函数。任何攻击者可调用 initialize 成为 owner。
初始化函数无 initializer 保护缺少 OpenZeppelin initializer 修饰符,可被多次调用。攻击者重新初始化,覆盖 owner 等关键变量。
初始化函数可被任何人调用即使有 initializer 检查,但未限制调用者(例如未要求 msg.sender == deployer)。攻击者可抢先调用,成为合约 owner。
构造函数在代理中无效逻辑合约的构造函数中设置了 owner,但代理通过 delegatecall 调用逻辑时,该构造函数未执行。代理合约的 owner 始终为 0,逻辑合约的构造函数设置无效。
部分初始化 / 初始化不完整initialize 函数只设置了部分变量,其他关键变量(如 pausedversion)仍为默认值。业务逻辑错误,可能被利用绕过检查。

3. 典型漏洞示例

3.1 未保护的初始化函数(可被任何人抢先调用)

// 漏洞逻辑合约(Solidity 0.8.x)
contract Logic {
    address public owner;
    bool public initialized;   // 手动标记,但未限制调用者

    function initialize(address _owner) public {
        require(!initialized, "Already initialized");
        owner = _owner;
        initialized = true;
    }

    function onlyOwner() public view {
        require(msg.sender == owner, "Not owner");
    }
}

攻击:攻击者在合约部署后立即调用 initialize(address(attacker)),成为 owner。而真正的管理员调用时会因为 initialized 为 true 而失败。

3.2 代理模式中构造函数失效

// 逻辑合约(错误示例)
contract Logic {
    address public owner;

    constructor() {
        owner = msg.sender;   // ❌ 此代码在代理模式中永远不会执行
    }
}

// 代理合约
contract Proxy {
    address public implementation;
    address public owner;      // 代理自己的存储,owner 始终为 0

    constructor(address _impl) {
        implementation = _impl;
        // 未调用 Logic.initialize()
    }

    fallback() external {
        (bool success,) = implementation.delegatecall(msg.data);
        require(success);
    }
}

后果:代理合约存储中的 owner 永远为 0x0。任何人都可以成为 owner 或直接调用需 onlyOwner 的函数(如果检查 owner 变量的话,会检查代理存储中的 owner,仍为 0)。

3.3 使用 OpenZeppelin Initializable 但未限制调用者

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract MyContract is Initializable {
    address public owner;

    function initialize() public initializer {
        owner = msg.sender;   // 未检查 msg.sender 是否为预期管理员
    }
}

若合约部署后,攻击者抢先在管理员之前调用 initialize(),将成为 owner。


4. 防御措施

防御策略具体实现
强制调用者initialize 中限制只有合约部署者或指定地址可调用(如 require(msg.sender == DEPLOYER))。但更好的做法:使用构造函数参数传递初始 owner,并通过 initializer 确保只调用一次。
使用 OpenZeppelin Initializable + 构造盐结合 initializer 修饰符,并在部署时使用 CREATE2 预先计算地址,确保只有管理员能调用初始化函数。
在代理合约中自动调用初始化在代理合约的构造函数中通过 delegatecall 调用逻辑合约的 initialize,确保创建代理后立即完成初始化。
禁用逻辑合约的初始化在逻辑合约的构造函数中直接调用 _disableInitializers()(OpenZeppelin 提供),防止逻辑合约本身被初始化(因为逻辑合约不应被直接使用)。
使用透明代理(Transparent Proxy)代理合约限制只有管理员才能调用升级函数和初始化函数,普通用户无法调用代理的 initialize
测试覆盖编写测试用例,确保初始化函数只能被正确调用一次,且未被调用时合约不可用。

安全初始化示例(推荐模式):

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract SafeContract is Initializable, OwnableUpgradeable {
    // 防止逻辑合约被初始化
    constructor() {
        _disableInitializers();
    }

    // 初始化函数,仅能被调用一次,且调用者成为 owner
    function initialize(address initialOwner) public initializer {
        __Ownable_init(initialOwner);   // 使用 OwnableUpgradeable 的标准初始化
    }
}

关键点

  • _disableInitializers() 确保逻辑合约自身不能被初始化(防止攻击者对逻辑合约直接调用)。
  • 使用 OpenZeppelin 的 OwnableUpgradeable,其 __Ownable_init 已包含 initializer 保护,并将 msg.sender 设为 owner。
  • 代理部署后,应尽快调用 initialize,最好在同一笔交易中完成。

5. 真实案例

  • EOS 众筹合约:早期以太坊上的 EOS 众筹合约因构造函数命名错误(拼写为 EOSToken 而非 EOSTokenPresale),导致初始化函数未执行,攻击者通过调用错误的函数成为 owner,卷走资金。
  • Parity 钱包库合约(2017):库合约的初始化函数未加保护,攻击者调用 initWallet 成为库的 owner,然后自毁库合约,导致所有依赖该库的钱包无法使用。
  • Harvest Finance(2020 年漏洞之一):部分策略合约的初始化函数未受保护,导致攻击者可重置关键参数。

6. 总结

初始化失败漏洞本质上是访问控制与生命周期管理的缺陷。在可升级合约盛行的今天,开发者必须牢记:

  • 构造函数在代理模式中无效,必须使用 initialize 函数。
  • initialize 必须同时具备 “仅一次”“仅授权者” 的双重保护。
  • 推荐使用 OpenZeppelin 的 InitializableOwnableUpgradeable 标准模块。
  • 部署时立即初始化,或在代理构造函数中完成初始化,避免出现时间窗口。

审计此类漏洞时,重点检查所有可能改变关键状态(如 owner、implementation)的初始化函数,确认它们不能被未授权或重复调用。