Skip to content

Commit ba4e8e8

Browse files
committed
chore: fix FLAG_ADDRESS security vulnerability
Fix bootstrap signature validation to require verified signature from Immutable signer and meet weight threshold, removing FLAG_ADDRESS support to prevent unauthorized wallet takeover on first transaction.
1 parent ebe0a91 commit ba4e8e8

2 files changed

Lines changed: 35 additions & 23 deletions

File tree

src/contracts/modules/commons/ModuleAuthDynamic.sol

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ abstract contract ModuleAuthDynamic is ModuleAuthUpgradable {
1818
uint256 rindex;
1919
bytes32 imageHash;
2020
uint256 totalWeight;
21-
bool immutableSignerContractFound;
21+
bool immutableSignerContractSigned;
2222
}
2323

2424
bytes32 public immutable INIT_CODE_HASH;
@@ -43,13 +43,17 @@ constructor(address _factory, address _startupWalletImpl, address _immutableSign
4343
* @return needsUpdate True if the image hash needs to be stored (first transaction)
4444
* @return imageHash The computed image hash from the signature
4545
*
46-
* @dev This function parses the signature, recovers/reads signer addresses, and validates them.
47-
* For defensive validation, each extracted address is compared against IMMUTABLE_SIGNER_CONTRACT.
48-
* If a match is found, it is recorded.
46+
* @dev This function parses the signature, recovers signer addresses from verified signatures, and validates them.
47+
* Only FLAG_SIGNATURE and FLAG_DYNAMIC_SIGNATURE are supported - FLAG_ADDRESS is intentionally not
48+
* supported to prevent attackers from including addresses without providing valid signatures.
4949
*
50-
* Special case: If this is the first transaction (nonce was 0, now 1 after increment) and the immutable
51-
* signer contract is one of the signers, the signature is automatically validated and approved without
52-
* checking the stored image hash. This allows the immutable signer to bootstrap the wallet on first use.
50+
* For each verified signature, the extracted address is compared against IMMUTABLE_SIGNER_CONTRACT.
51+
* If a match is found after signature verification, it is recorded.
52+
*
53+
* Special case: If this is the first transaction (nonce was 0, now 1 after increment), the immutable
54+
* signer contract has provided a valid signature, AND the weight threshold is met, the signature is
55+
* automatically validated and approved without checking the stored image hash. This allows the immutable
56+
* signer to bootstrap the wallet on first use while preventing unauthorized bootstrap attacks.
5357
*/
5458
function _signatureValidationWithUpdateCheck(
5559
bytes32 _hash,
@@ -66,7 +70,7 @@ constructor(address _factory, address _startupWalletImpl, address _immutableSign
6670
rindex: rindex,
6771
imageHash: bytes32(uint256(threshold)),
6872
totalWeight: 0,
69-
immutableSignerContractFound: false
73+
immutableSignerContractSigned: false
7074
});
7175

7276
// Iterate until the image is completed
@@ -75,17 +79,22 @@ constructor(address _factory, address _startupWalletImpl, address _immutableSign
7579
uint256 flag; uint256 addrWeight; address addr;
7680
(flag, addrWeight, state.rindex) = _signature.readUint8Uint8(state.rindex);
7781

78-
if (flag == FLAG_ADDRESS) {
79-
// Read plain address
80-
(addr, state.rindex) = _signature.readAddress(state.rindex);
81-
} else if (flag == FLAG_SIGNATURE) {
82+
// Note: FLAG_ADDRESS is intentionally not supported in this module to prevent
83+
// attackers from including the immutable signer address without providing a valid signature.
84+
// Only FLAG_SIGNATURE and FLAG_DYNAMIC_SIGNATURE are allowed.
85+
if (flag == FLAG_SIGNATURE) {
8286
// Read single signature and recover signer
8387
bytes memory signature;
8488
(signature, state.rindex) = _signature.readBytes66(state.rindex);
8589
addr = recoverSigner(_hash, signature);
8690

87-
// Acumulate total weight of the signature
91+
// Accumulate total weight of the signature
8892
state.totalWeight += addrWeight;
93+
94+
// Check if this signer is the immutable signer contract (only after signature verification)
95+
if (addr == IMMUTABLE_SIGNER_CONTRACT) {
96+
state.immutableSignerContractSigned = true;
97+
}
8998
} else if (flag == FLAG_DYNAMIC_SIGNATURE) {
9099
// Read signer
91100
(addr, state.rindex) = _signature.readAddress(state.rindex);
@@ -99,25 +108,27 @@ constructor(address _factory, address _startupWalletImpl, address _immutableSign
99108
(signature, state.rindex) = _signature.readBytes(state.rindex, size);
100109
require(isValidSignature(_hash, addr, signature), "ModuleAuthDynamic#_signatureValidation: INVALID_SIGNATURE");
101110

102-
// Acumulate total weight of the signature
111+
// Accumulate total weight of the signature
103112
state.totalWeight += addrWeight;
113+
114+
// Check if this signer is the immutable signer contract (only after signature verification)
115+
if (addr == IMMUTABLE_SIGNER_CONTRACT) {
116+
state.immutableSignerContractSigned = true;
117+
}
104118
} else {
105119
revert("ModuleAuthDynamic#_signatureValidation INVALID_FLAG");
106120
}
107121

108-
// Check if this signer is the immutable signer contract
109-
if (addr == IMMUTABLE_SIGNER_CONTRACT) {
110-
state.immutableSignerContractFound = true;
111-
}
112-
113122
// Write weight and address to image
114123
state.imageHash = keccak256(abi.encode(state.imageHash, addrWeight, addr));
115124
}
116125

117-
// Check if this is the first transaction (nonce was 0 before increment) and immutable signer contract is one of the signers
126+
// Check if this is the first transaction (nonce was 0 before increment) and immutable signer contract
127+
// has provided a valid signature. The immutable signer must have actually signed (not just be listed
128+
// as an address) and the total weight must meet the threshold to prevent unauthorized bootstrap attacks.
118129
// Note: _validateNonce increments the nonce before _signatureValidation is called, so we check for 1, not 0
119130
uint256 currentNonce = uint256(ModuleStorage.readBytes32Map(NonceKey.NONCE_KEY, bytes32(uint256(0))));
120-
if (currentNonce == 1 && state.immutableSignerContractFound) {
131+
if (currentNonce == 1 && state.immutableSignerContractSigned && state.totalWeight >= threshold) {
121132
return (true, true, state.imageHash);
122133
}
123134

tests/ModuleAuthDynamic.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -568,9 +568,10 @@ describe('ModuleAuthDynamic Bootstrap Flow', () => {
568568
false
569569
)
570570

571-
// Should fail because threshold requires both signatures but only ImmutableSigner signed
571+
// Should fail because FLAG_ADDRESS is not supported in ModuleAuthDynamic
572+
// (removed to prevent attackers from including addresses without signatures)
572573
await expect(wallet.execute([transaction2], 1, signature2)).to.be.revertedWith(
573-
'ModuleCalls#execute: INVALID_SIGNATURE'
574+
'ModuleAuthDynamic#_signatureValidation INVALID_FLAG'
574575
)
575576
})
576577
})

0 commit comments

Comments
 (0)