Introdução
Em agosto de 2021, a Poly Network, uma plataforma de finanças descentralizadas, foi hackeada em $611 milhões. O invasor explorou uma vulnerabilidade de controle de acesso entre dois contratos inteligentes importantes da Poly para obter controle sobre os fundos. Você pode estar se perguntando o que é controle de acesso em um contrato inteligente, especialmente em Solidity.
Controle de acesso em contratos inteligentes Solidity significa controlar quem pode fazer determinadas coisas com o contrato com base em suas permissões. Isso é crucial para contratos inteligentes, pois determina quem pode criar novos tokens, votar em ideias, fazer saques, interromper transferências e mais.
Portanto, é crucial entender como configurá-lo corretamente, ou os invasores aproveitarão seu sistema.
O que é uma Vulnerabilidade de Controle de Acesso?
Uma vulnerabilidade de controle de acesso é um tipo de falha de segurança que permite que usuários sem permissão interajam e alterem dados ou funções em um contrato inteligente. Essa vulnerabilidade pode ocorrer quando há uma brecha nas restrições de acesso e atribuição de funções em um contrato inteligente Solidity.
Muitos projetos descentralizados perderam grandes quantias de dinheiro devido a esse problema de segurança simples. Mas esse risco de segurança pode ser evitado. Na próxima seção, abordaremos algumas estratégias e exemplos de mitigação.
Estratégias e Exemplos de Mitigação
- Utilize a instrução Require de forma eficaz:
// Este contrato é vulnerável a uma vulnerabilidade de controle de acesso
contract AccessControlVulnerable {
mapping(address => uint256) private balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
// Falta a instrução require para verificar o saldo
uint256 balance = balances[msg.sender];
// Sem verificação de saldo suficiente antes da retirada
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
function getBalance() public view returns (uint256) {
return balances[msg.sender];
}
}
Ao analisar o contrato acima, percebe-se que a função withdraw tem uma falha, uma vulnerabilidade de controle de acesso, porque não inclui uma instrução require para verificar se o remetente tem saldo suficiente antes de permitir a retirada.
Isso significa que qualquer pessoa pode retirar qualquer quantia, mesmo que não tenha saldo suficiente em sua conta.
Como corrigimos essa vulnerabilidade? Incluindo uma instrução require para verificar se o remetente tem saldo suficiente antes de permitir a retirada.
Solução
// Este contrato possui controle de acesso aprimorado
contract AccessControlImproved {
mapping(address => uint256) private balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
require(amount <= balances[msg.sender], "Saldo insuficiente");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
function getBalance() public view returns (uint256) {
return balances[msg.sender];
}
}
- Utilize Modificadores de Acesso:
contract AccessControlVulnerability {
address public owner;
uint256 public secretNumber;
constructor(uint256 _initialNumber) {
owner = msg.sender;
secretNumber = _initialNumber;
}
function setSecretNumber(uint256 _newNumber) public {
// Sem modificador de acesso, então qualquer pessoa pode alterar o secretNumber
secretNumber = _newNumber;
}
function getSecretNumber() public view returns (uint256) {
return secretNumber;
}
}
Observe o exemplo acima: a função setSecretNumber não possui um modificador de acesso como *onlyOwner *, o que significa que qualquer pessoa pode chamá-la para alterar o *secretNumber *. Isso é uma vulnerabilidade porque a intenção do contrato era provavelmente permitir apenas ao proprietário modificar o número secreto, mas devido à ausência de controle de acesso, qualquer pessoa pode alterá-lo.
Aqui está a solução:
contract SecureAccessControl {
address public owner;
uint256 public secretNumber;
constructor(uint256 _initialNumber) {
owner = msg.sender;
secretNumber = _initialNumber;
}
modifier onlyOwner() {
require(msg.sender == owner, "Apenas o proprietário pode chamar esta função");
_;
}
function setSecretNumber(uint256 _newNumber) public onlyOwner {
secretNumber = _newNumber;
}
function getSecretNumber() public view returns (uint256) {
return secretNumber;
}
}
Agora, atualizamos o contrato e adicionamos o modificador onlyOwner. Esse modificador verifica se o chamador de uma função é o proprietário do contrato antes de permitir que a função prossiga. Ao adicionar esse modificador à função setSecretNumber, garantimos que apenas o proprietário do contrato pode alterar o número secreto.
- Use a Interface de Controle de Acesso OpenZeppelin:
O contrato abaixo é um contrato VotingSystem e carece de controle de acesso adequado para a função vote. Isso significa que qualquer endereço pode chamar a função vote várias vezes, evitando quaisquer limitações sobre o número de votos que um único usuário deveria poder emitir. Isso representa uma falha no sistema de votação, já que não impede votos não autorizados ou excessivos.
// contrato vulnerável
contract VotingSystem {
mapping(address => bool) private hasVoted;
uint256 public totalVotes;
constructor() {
totalVotes = 0;
}
function vote() public {
// Nenhuma verificação de controle de acesso; qualquer um pode votar várias vezes
require(!hasVoted[msg.sender], "Você já votou");
hasVoted[msg.sender] = true;
totalVotes++;
}
function getVoteStatus(address voter) public view returns (bool) {
return hasVoted[voter];
}
}
A vulnerabilidade no contrato VotingSystem pode ser mitigada usando uma interface de controle de acesso da OpenZeppelin.
// contrato seguro
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureVotingSystem is Ownable {
mapping(address => bool) private hasVoted;
uint256 public totalVotes;
constructor() {
totalVotes = 0;
}
function vote() public onlyOwner {
require(!hasVoted[msg.sender], "Você já votou");
hasVoted[msg.sender] = true;
totalVotes++;
}
function getVoteStatus(address voter) public view returns (bool) {
return hasVoted[voter];
}
}
Você pode ver que importamos o contrato Ownable da OpenZeppelin, que fornece um sistema simples de controle de acesso. Usando o modificador onlyOwner, garantimos que apenas o proprietário do contrato (quem o implantou) pode chamar a função vote. Isso impede que usuários não autorizados votem várias vezes e implementa controle de acesso adequado sobre o processo de votação.
- Implemente um Sistema de Controle de Acesso Baseado em Funções Bem-Projetadas:
O controle de acesso baseado em funções é um sistema onde diferentes funções são atribuídas a usuários ou entidades e permissões de acesso são concedidas com base nessas funções. Implementar um sistema de controle de acesso baseado em funções bem-projetado pode melhorar significativamente a segurança de um contrato inteligente, impedindo o acesso não autorizado e minimizando o impacto de vulnerabilidades de controle de acesso.
Para uma compreensão mais clara, vamos analisar o exemplo abaixo:
Contrato vulnerável:
pragma solidity ^0.8.0;
// Contrato Vulnerável - LendingData.sol
contract LendingData {
address public owner;
constructor() {
owner = msg.sender;
}
// Função vulnerável permitindo que o proprietário transfira a propriedade
function transferOwnership(address newOwner) public {
require(msg.sender == owner, "Apenas o proprietário pode transferir a propriedade");
owner = newOwner;
}
// Outras funções...
}
Contrato de Solução:
pragma solidity ^0.8.0;
// Contrato de Solução - LendingData.sol
contract LendingDataSolution {
address public owner;
mapping(address => bool) public managers; // Função adicional
constructor() {
owner = msg.sender;
managers[msg.sender] = true; // Atribuir o criador como gerente
}
modificador onlyOwner() {
require(msg.sender == owner, "Apenas o proprietário pode chamar esta função");
_;
}
modificador onlyManager() {
require(managers[msg.sender], "Apenas gerentes podem chamar esta função");
_;
}
// Funções com controle de acesso adequado
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
function addManager(address newManager) public onlyOwner {
managers[newManager] = true;
}
// Outras funções...
}
Nos dois contratos acima, você notará que no contrato vulnerável, o contrato LendingData permite apenas ao proprietário transferir a propriedade e realizar outras funções. Este é um exemplo de controle de acesso baseado em funções inadequadas, pois dá muita autoridade ao proprietário, o que pode levar a problemas de segurança se a conta do proprietário for comprometida.
O contrato de solução, LendingDataSolution, implementa um mecanismo de controle de acesso mais abrangente. Ele introduz o conceito de gerentes como uma função adicional.
O proprietário ainda pode realizar ações críticas, mas a capacidade de adicionar gerentes permite que o proprietário delegue responsabilidades específicas sem abrir mão do controle total. Os modificadores onlyOwner e onlyManager garantem que apenas as funções apropriadas possam chamar funções específicas, reduzindo os riscos potenciais de segurança.
Essa solução destaca a importância de aplicar políticas adequadas de controle de acesso e evitar um único ponto de autoridade.
- Lista Branca (Whitelisting):
Quando se trata de mitigar vulnerabilidades relacionadas ao controle de acesso, a lista branca é outra técnica essencial. A lista branca é usada para especificar uma lista de endereços ou entidades que têm permissão para realizar determinadas ações ou acessar recursos específicos em um contrato inteligente. Ao fazer isso, apenas esses endereços que foram pré-aprovados têm acesso, reduzindo assim a superfície de ataque e as vulnerabilidades potenciais.
Por exemplo, considere um mercado de NFT que permite aos usuários interagir com um contrato inteligente para criar e gerenciar colecionáveis digitais (NFTs). Aqui está um contrato vulnerável onde qualquer pessoa pode criar NFTs sem controle de acesso adequado:
Neste contrato vulnerável, a função mintNFT não possui nenhum sistema de controle de acesso. Qualquer pessoa pode chamar esta função e criar novos NFTs, o que poderia levar a criação não autorizada ou maliciosa nesta situação.
// Contrato Vulnerável - Collectibles.sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract Collectibles is ERC721 {
uint256 public tokenIdCounter;
constructor() ERC721("Collectibles", "COLL") {}
function mintNFT(address to) public {
tokenIdCounter++;
_mint(to, tokenIdCounter);
}
}
Agora, vamos usar a lista branca para impedir a criação não autorizada e manter o controle sobre quem pode criar NFTs:
// Contrato de Solução - Collectibles.sol
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract CollectiblesSolution is ERC721, Ownable {
uint256 public tokenIdCounter;
mapping(address => bool) public whitelistedMinters;
constructor() ERC721("Collectibles", "COLL") {}
function mintNFT(address to) public onlyWhitelistedMinter {
tokenIdCounter++;
_mint(to, tokenIdCounter);
}
function addToWhitelist(address minter) public onlyOwner {
whitelistedMinters[minter] = true;
}
function removeFromWhitelist(address minter) public onlyOwner {
whitelistedMinters[minter] = false;
}
modifier onlyWhitelistedMinter() {
require(whitelistedMinters[msg.sender], "Apenas criadores autorizados podem chamar esta função");
_;
}
}
Portanto, no contrato de solução, usamos a lista branca mantendo um mapeamento de endereços autorizados a criar NFTs. A função mintNFT agora possui o modificador onlyWhitelistedMinter, que garante que apenas endereços na lista branca podem chamar esta função. Além disso, o proprietário do contrato pode gerenciar a lista branca usando as funções addToWhitelist e removeFromWhitelist.
Conclusão
Para resumir tudo, as vulnerabilidades de controle de acesso são um vetor de ataque muito popular usado por hackers para explorar contratos inteligentes, e é importante gerenciar o acesso e as permissões em contratos inteligentes de maneira eficaz. O uso de técnicas de mitigação, como Sistemas de Controle de Acesso Baseados em Funções, modificadores de acesso, interfaces de controle de acesso OpenZeppelin, declarações require e listas de permissões, pode ser fundamental para minimizar o risco de ser hackeado.
function getSecretNumber() public view returns (uint256) {
return secretNumber;
}
Este artigo foi escrito por Natachi Nnamaka e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.
Top comments (0)