Skip to content

Exploit and Remediate Function Visibility Vulnerabilities in Smart Contracts

Smart contracts are used by Ethereum to handle processed based on transactions. Many companies, banks and crypto enthusiasts use them for selling their services or products. Those contracts are written by developers and some of those contains vulnerabilities. One of those is the Visibility issue.

What is the Function Visibility Vulnerability?

A vulnerability which happens when the wrong visibility has been set in a function. The visibility is defined as 4 different properties, private, internal, external, public. Those are differently used based on the purpose of the function. Some functions are made to be called by users, others are made to be called by the contract only.

What do the different Visibility values mean?

In smart contracts, each visibility has a purpose. A function which retries funds from the main smart contract storage and transfers it to a user, should be protected by the required “permissions”, to prevent the exploitation from an unauthorized user. The different values are:

  • Private: The function is only accessible by the contract and non of the derived / child contracts. It is used for functions and state variables.
  • Internal: The function is accessible by the contract but also from the derived / child contracts. It is used for functions and state variables.
  • External: The function is accessible by other contracts or transactions and they can not be called internally (except by using: this.extFunctionName()). It is only used for functions.
  • Public: The function is accessible by the contract and any other contract. This is the broadest value and is used for functions and state variables.

How to exploit a function vulnerable to misconfigured visibility?

Below is a simple example with a guessing game. The idea is that users would try to guess the correct password to win something. In this case when the wrong password is guessed, it returns false, while if it is guessed correctly, it returns True. It is visible the the setSecretNumber function, has visibility of public, which means that can be called by everyone. This allows a malicious actor to set the value of their choice and then be able to guess it, winning the reward.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

contract VulnerableContract {
    uint256 private secretNumber;

    constructor() {
        secretNumber = 812;
    }

    function setSecretNumber(uint256 _newNumber) public  {
        secretNumber = _newNumber;
    }

    function guessNumber(uint256 _guess) public view returns (bool) {
        return (_guess == secretNumber);
    }
}

After deploying the contract, the user is allowed to call both functions. To start with it, on the left it is visible that the guessNumber was called with the wrong number and returned an error.

Exploiting the misconfigured public function
Guessing the wrong number returns false

After calling the misconfigured setSecretNumber function, the secret number was set to 12. Then using the guessNumber function, the updated secret number matched the number previously supplied and returned True.

Exploiting the misconfigured public function
Number updated and guessed correctly

An real life example of this vulnerability can be seen been exploited at the Parity Wallet hack of 2017 which resulted in 150.000 Ethereum to get stolen. The library did not set the correct visibility for the initWallet function on line 216, which defaulted to public, since before Solidity version 0.5, there was not requirement for every function to have its visibility defined.

  // constructor - just pass on the owner array to the multiowned and
  // the limit to daylimit
  function initWallet(address[] _owners, uint _required, uint _daylimit) {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
  }

Additionally, the contract had a delegatecall function, on lines 424, which allowed another contract to act as a proxy. This allowed a malicious actor set themselves as the owners by using the initWallet function and then withdraw the Ethereum stored in the contract.

  // gets called when no other function matches
  function() payable {
    // just being sent some cash?
    if (msg.value > 0)
      Deposit(msg.sender, msg.value);
    else if (msg.data.length > 0)
      _walletLibrary.delegatecall(msg.data);
  }

Remediate Function Visibility Vulnerabilities

The way to remediate such vulnerabilities is to understand what is the purpose of the function / variable and when it is used. There is no magic function which will adjust the correct visibility. In the example above, an updated contract would have the public visibility of the function set to private, like shown below:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

contract VulnerableContract {
    uint256 private secretNumber;

    constructor() {
        secretNumber = 123;
    }

    function setSecretNumber(uint256 _newNumber) private  {
        secretNumber = _newNumber;
    }

    function guessNumber(uint256 _guess) public view returns (bool) {
        return (_guess == secretNumber);
    }
}

Overly permissive visibility can lead to vulnerabilities, as in the case of functions that should not be exposed to external contracts or users. On the other hand, restrictive visibility may limit a contract’s functionality and interoperability with other contracts.

Was this post helpful?