Back to Foundry

Unprotected initializer

crates/lint/docs/unprotected-initializer.md

1.8.11.8 KB
Original Source

Unprotected initializer

Severity: High ID: unprotected-initializer

Flags upgradeable contracts whose public or external initializer can still be called directly on an implementation contract that exposes a destructive entry point.

What it does

Reports initializer-like functions that:

  • are public or external;
  • are marked with initializer or reinitializer;
  • write state directly or through an internal helper; and
  • are in an implementation whose constructor does not call an inherited _disableInitializers(); and
  • are in a contract with a public or external delegatecall, callcode, or selfdestruct path that is not restricted to proxy calls with onlyProxy.

Why is this bad?

An attacker can initialize the implementation directly, take ownership, and invoke its destructive entry point. Destroying or corrupting an implementation can disable every proxy that delegates to it.

Example

Bad

solidity
contract Vault is Initializable {
    address public owner;

    function initialize(address owner_) public initializer {
        owner = owner_;
    }

    function execute(address target, bytes calldata data) external {
        (bool ok,) = target.delegatecall(data);
        require(ok);
    }
}

Good

solidity
contract Vault is Initializable {
    address public owner;

    constructor() {
        _disableInitializers();
    }

    function initialize(address owner_) public initializer {
        owner = owner_;
    }
}

Notes

The lint is intentionally local: it does not inspect deployment scripts to prove whether a proxy is initialized atomically. It focuses on implementation contracts that remain directly initializable and can reach code paths that may destroy or replace implementation state.

The onlyProxy exemption is a name-based heuristic for common UUPS implementations.