Calling all Web3 developers and security enthusiasts! Contribute and gain recognition on web3sec.newsThe ultimate open-source platform for sharing Web3 security insights. Publish your blog topics, from the latest news to blockchain tech and audits, and receive feedback and exciting opportunities.
Join the vibrant Web3 security community today 🤝
Table of contents
Open Table of contents
Signature Replay
Understading the vulneribility
The signature replay vulnerability in the
VulnerableContract
allows an attacker to reuse a valid signature multiple times. Once an attacker acquires a valid signature for a specific update, they can repeatedly call thesetValue
function with the same signature to modify the value multiple times. This unauthorized and unintended behavior occurs because the contract doesn’t have safeguards to prevent the reuse of the same signature.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract VulnerableContract {
using ECDSA for bytes32;
address public owner;
uint public value;
constructor(address _owner) payable {
owner = _owner;
}
function setValue(uint _newValue, bytes memory _sig) external {
bytes32 txHash = getTxHash(_newValue);
require(_checkSig(_sig, txHash), "Invalid signature");
value = _newValue;
}
function getTxHash(uint _newValue) public view returns (bytes32) {
return keccak256(abi.encodePacked(_newValue));
}
function _checkSig(bytes memory _sig, bytes32 _txHash) private view returns (bool) {
bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();
address signer = ethSignedHash.recover(_sig);
return signer == owner;
}
}
Mitigate the vulneribility
To mitigate this vulnerability, it is crucial to implement measures such as utilizing expiry timestamps or storing and tracking transaction hashes to ensure the uniqueness and validity of each transaction, thereby preventing unauthorized replay attacks.
There are some ways to prevent this vulneribility
1.By storing transaction hash
We store the hash of each executed transaction and prevent re-execution of the same transaction. The HashMitigatedContract
prevents the signature replay attack by tracking executed transactions through a mapping that stores the transaction hash
check this part in setValue
function —>
require(!_isTransactionExecuted(txHash), "Transaction has already been executed");
Before executing a transaction, the contract checks if the transaction hash has already been marked as executed, ensuring that a transaction can only be executed once. After a successful execution, the contract marks the transaction as executed, preventing it from being replayed in the future. This simple but effective mitigation ensures that each signature can only be used once, maintaining the integrity of the contract state and security.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract HashMitigatedContract {
using ECDSA for bytes32;
address public owner;
uint public value;
mapping(bytes32 => bool) public executedTransactions;
constructor(address _owner) payable {
owner = _owner;
}
function setValue(uint _newValue, bytes memory _sig) external {
bytes32 txHash = getTxHash(_newValue);
require(!_isTransactionExecuted(txHash), "Transaction has already been executed");
require(_checkSig(_sig, txHash), "Invalid signature");
value = _newValue;
_markTransactionExecuted(txHash);
}
function getTxHash(uint _newValue) public view returns (bytes32) {
return keccak256(abi.encodePacked(_newValue));
}
function _isTransactionExecuted(bytes32 _txHash) private view returns (bool) {
return executedTransactions[_txHash];
}
function _markTransactionExecuted(bytes32 _txHash) private {
executedTransactions[_txHash] = true;
}
function _checkSig(bytes memory _sig, bytes32 _txHash) private view returns (bool) {
bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();
address signer = ethSignedHash.recover(_sig);
return signer == owner;
}
}
2.By Using expiry timestamp
The ExpiryMitigatedContract
introduces an expiry timestamp to ensure that a transaction can only be executed before a certain time. This approach helps prevent replay attacks by setting a time limit on the validity of a signature. When the setValue function is called, it checks whether the current block timestamp is before or equal to the provided _expiry time. If the transaction has expired, the contract rejects it with the message “Transaction has expired.” This ensures that a transaction can only be executed within the specified time frame. This mitigation provides an additional layer of security to prevent replay attacks by making the signatures time-sensitive and limiting their validity based on the expiration timestamp.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract ExpiryMitigatedContract {
using ECDSA for bytes32;
address public owner;
uint public value;
constructor(address _owner) payable {
owner = _owner;
}
function setValue(uint _newValue, bytes memory _sig, uint _expiry) external {
require(block.timestamp <= _expiry, "Transaction has expired");
bytes32 txHash = getTxHash(_newValue, _expiry);
require(_checkSig(_sig, txHash), "Invalid signature");
value = _newValue;
}
function getTxHash(uint _newValue, uint _expiry) public view returns (bytes32) {
return keccak256(abi.encodePacked(_newValue, _expiry));
}
function _checkSig(bytes memory _sig, bytes32 _txHash) private view returns (bool) {
bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();
address signer = ethSignedHash.recover(_sig);
return signer == owner;
}
}
There can be other attacks like cross chain replay in that case valid signature in one chain can be used in another chain and to prevent this kind of attack we need to use
chainId
in signature
Thank you for reading ✌🏻
Written By
- Name: Hemendra Sharma
- Twitter: CodeNinja04