Deep EVM #4 : Primitives de sécurité — msg.sender, contrôle d'accès et réentrance
Engineering Team
L’identité dans l’EVM : msg.sender
Dans l’EVM, l’identité est déterminée par l’opcode CALLER, accessible en Solidity via msg.sender. Cet opcode retourne l’adresse de 20 octets qui a initié le contexte d’appel actuel.
Point crucial : msg.sender change à chaque frontière d’appel. Si l’utilisateur A appelle le contrat B qui appelle le contrat C, alors dans C, msg.sender est l’adresse de B, pas de A. L’adresse originale est disponible via tx.origin (opcode ORIGIN), mais l’utiliser pour l’authentification est une vulnérabilité connue.
Pourquoi tx.origin est dangereux
// VULNÉRABLE : attaque de phishing
function withdraw() external {
require(tx.origin == owner); // Ne faites JAMAIS ça
payable(msg.sender).transfer(address(this).balance);
}
Un contrat malveillant peut appeler withdraw() — tx.origin sera toujours le propriétaire s’il est celui qui a initié la transaction racine.
Patterns de contrôle d’accès
Modificateur Ownable
Le pattern le plus simple : un seul propriétaire ayant le contrôle total.
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function criticalAction() external onlyOwner {
// Seul le propriétaire peut exécuter
}
Contrôle d’accès basé sur les rôles (RBAC)
Pour des permissions plus granulaires, OpenZeppelin’s AccessControl :
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
La réentrance : l’attaque classique
La réentrance se produit quand un contrat effectue un appel externe avant de terminer ses mises à jour d’état internes. Le contrat appelé peut rappeler la fonction vulnérable et exploiter l’état incohérent.
L’attaque DAO (2016)
// VULNÉRABLE
function withdraw() external {
uint256 balance = balances[msg.sender];
(bool success,) = msg.sender.call{value: balance}("");
require(success);
balances[msg.sender] = 0; // Trop tard ! L'attaquant a déjà rappelé
}
L’attaquant déploie un contrat dont la fonction receive() rappelle withdraw() — drainant le contrat à chaque itération avant que le solde ne soit mis à zéro.
Protection : pattern Checks-Effects-Interactions
function withdraw() external {
uint256 balance = balances[msg.sender]; // Check
balances[msg.sender] = 0; // Effect
(bool success,) = msg.sender.call{value: balance}(""); // Interaction
require(success);
}
Mettez à jour l’état AVANT l’appel externe. Même si l’attaquant rappelle, le solde est déjà à zéro.
Protection : verrou de réentrance
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "Reentrant call");
_locked = 2;
_;
_locked = 1;
}
STATICCALL : la protection en lecture seule
L’opcode STATICCALL (EIP-214) garantit qu’un sous-appel ne peut modifier aucun état. Tout opcode de modification (SSTORE, CREATE, LOG, SELFDESTRUCT, CALL avec valeur) provoque un revert immédiat.
Utilisez STATICCALL pour tous les appels de lecture vers des contrats externes non fiables.
Conclusion
La sécurité dans l’EVM commence par comprendre les primitives : CALLER pour l’identité, les patterns de contrôle d’accès pour les permissions, et le pattern CEI (Checks-Effects-Interactions) pour la protection contre la réentrance. Ces mécanismes constituent le fondement sur lequel repose la sécurité de tout smart contract.
Dans le prochain article, nous introduirons Yul — le langage d’assemblage secret de Solidity.