• YouTube Channel
  • System Status
  • VS Code Extension
  • Default Visibility

    Overview

    What is the Default Visibility vulnerability?

    In Solidity, visibility can be specified for state variables and functions on smart contracts. If visibility is not specified for a state variable, it is internal by default in modern versions of Solidity, meaning that that contract state can be read and modified by the contract and any that inherit from it. If data should not be modified by inheriting contracts, it should be marked as private. Failure to explicitly mark state variable visibility in particular may lead to situations where data is accessible or modifiable in unintended contexts. In general, explicitly marking the visibility is a good way to express developer intent and can help avoid inadvertent pitfalls.

    Further reading: Solidity Documentation: Visibility and Getters

    Technical example of vulnerable code

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract AccessControl {
          // Mapping of address to bool indicating whether the address has admin access
          // By not specifying visibility, `isAdmin` is `internal` by default.
          mapping(address => bool) isAdmin;
    
          event AdminRightsGranted(address indexed to);
          event AdminRightsRevoked(address indexed from);
    
          constructor() {
              // Grant admin rights to the contract deployer
              isAdmin[msg.sender] = true;
          }
    
          // Modifier to restrict function access to only admins
          modifier onlyAdmin() {
              require(isAdmin[msg.sender], "Not an admin");
              _;
          }
    
          // Function to grant admin rights to an address
          function grantAdminRights(address _user) external onlyAdmin {
              isAdmin[_user] = true;
              emit AdminRightsGranted(_user);
          }
    
          // Function to revoke admin rights from an address
          function revokeAdminRights(address _user) external onlyAdmin {
              isAdmin[_user] = false;
              emit AdminRightsRevoked(_user);
          }
    
          // Check if an address is an admin
          function checkAdmin(address _user) external view returns (bool) {
              return isAdmin[_user];
          }
      }
    
      contract MaliciousInheritor is AccessControl {
          constructor() AccessControl() {}
    
          // Inadvertently or maliciously reset admin rights
          function resetAllAdmins() external {
              // A direct access like this could lead to unintended consequences
              // since `isAdmin` is `internal` and can be accessed here
              address[] memory admins = getAllAdmins();
              for (uint i = 0; i < admins.length; i++) {
                  isAdmin[admins[i]] = false;
              }
          }
    
          // Placeholder for actual implementation
          function getAllAdmins() internal pure returns (address[] memory) {
              // This would be implemented to return the list of all admin addresses
              return new address[](0); // Simplified for example purposes
          }
      }
    
    

    In the example above, contract AccessControl defines a state variable isAdmin, without specifying its visibility. This variable, which is meant to control admin rights for individual addresses, is thus internal by default, meaning contracts inheriting from AccessControl can modify this variable. In this case, the MaliciousInheritor contract exploits this access to enable resetting of all admin rights, which was not intended by the developer.

    Technical example of how to fix the vulnerability

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract AccessControlUpdated {
          // Mapping of address to bool indicating whether the address has admin access
          // By not specifying visibility, `isAdmin` is `internal` by default.
          mapping(address => bool) private isAdmin;
    
          event AdminRightsGranted(address indexed to);
          event AdminRightsRevoked(address indexed from);
    
          constructor() {
              // Grant admin rights to the contract deployer
              isAdmin[msg.sender] = true;
          }
    
          // Modifier to restrict function access to only admins
          modifier onlyAdmin() {
              require(isAdmin[msg.sender], "Not an admin");
              _;
          }
    
          // Function to grant admin rights to an address
          function grantAdminRights(address _user) external onlyAdmin {
              isAdmin[_user] = true;
              emit AdminRightsGranted(_user);
          }
    
          // Function to revoke admin rights from an address
          function revokeAdminRights(address _user) external onlyAdmin {
              isAdmin[_user] = false;
              emit AdminRightsRevoked(_user);
          }
    
          // Check if an address is an admin
          function checkAdmin(address _user) external view returns (bool) {
              return isAdmin[_user];
          }
      }
    
    

    In the revised example above, contract AccessControlUpdated has marked the isAdmin state variable as private, meaning only the contract itself can modify it.