Reproduced Exploit
Etherspot — a session key spends 1000 GLUE past a 100 GLUE limit
validateSessionKeyParams is public. It forwards the userOp into _validateSingleCall, and that helper transfers amount out of the wallet after checking only that the session key is bound to that wallet. It never compares amount to sessionLimit. A key whose limit is 100 GLUE therefore spends N = 1000…
Chain
Other
Category
access-control
Date
Jul 2025
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 61407-c-05-in-credibleaccountmodule-the-validatesessionkeyparams-f. Standalone Foundry PoC and full write-up: 61407-c-05-in-credibleaccountmodule-the-validatesessionkeyparams-f_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/access-control/missing-auth · vuln/logic/missing-check · vuln/access-control/missing-modifier
Reproduction: a self-contained Foundry PoC that compiles and runs offline with only
forge-std— no fork and no RPC. Trace: output.txt. PoC: test/61407-c-05-in-credibleaccountmodule-the-validatesessionkeyparams-f_exp.sol.
Key info#
| Impact | HIGH — a session limited to 100 GLUE spends 1000 GLUE out of the wallet |
| Protocol | Etherspot CredibleAccountModule |
| Vulnerable code | public validateSessionKeyParams / _validateSingleCall |
| Bug class | Session parameters are not checked against the spend limit |
| Finding | Shieldify — Credible Account Module, July 2025 · id 61407 / C-05 |
| Report | Etherspot-CredibleAccountModule-Security-Review.md |
| Source | AuditVault finding |
| Status | Fixed by Etherspot. Reproduced here as a local synthetic. |
| Compiler | solc 0.8.27, cancun, via IR |
TL;DR#
validateSessionKeyParams is public. It forwards the userOp into _validateSingleCall, and that helper transfers amount out of the wallet after checking only that the session key is bound to that wallet. It never compares amount to sessionLimit. A key whose limit is 100 GLUE therefore spends N = 1000 GLUE. The caller receives 1000 GLUE and the wallet balance goes to 0.
The fixed module reverts SessionParamsRejected on the same userOp. The 1000 GLUE stays in the wallet.
Vulnerable code#
The reviewed function is public and does not validate the spend. Reduced to the single-call path the report names:
function validateSessionKeyParams(address _sessionKey, PackedUserOperation calldata userOp) public returns (bool) {
if (isSessionClaimed[_sessionKey]) return false;
return _validateSingleCall(_sessionKey, userOp.sender, userOp.amount);
}
function _validateSingleCall(address sessionKey, address sender, uint256 amount) internal returns (bool) {
require(sessionWallet[sessionKey] == sender, "wallet");
// @> VULN: amount is not checked against sessionLimit, so the key spends past it
token.transferFrom(sender, msg.sender, amount);
sessionSpent[sessionKey] = amount;
isSessionClaimed[sessionKey] = true;
return true;
}
The fix rejects the params before the transfer:
function _validateSingleCall(address sessionKey, address sender, uint256 amount) internal returns (bool) {
require(sessionWallet[sessionKey] == sender, "wallet");
+ if (amount > sessionLimit[sessionKey]) revert SessionParamsRejected();
token.transferFrom(sender, msg.sender, amount);
Shieldify also asked for the function to be internal. The control keeps it callable and rejects an over-limit amount, which is the harm this reproduction measures.
Attack walkthrough#
- The wallet is minted 1000 GLUE and approves the module for 1000.
enableSessionbinds the key to the wallet withsessionLimit = 100 GLUE.- The caller, who is not the wallet, calls the public
validateSessionKeyParamswithamount = 1000. - The helper checks
sessionWallet[key] == senderand thentransferFroms 1000 GLUE to the caller. sessionSpentis 1000. The wallet balance is 0. The caller holds 1000 GLUE, which is 900 above the limit.
Control: the same enable and the same userOp revert SessionParamsRejected. The wallet still holds 1000 GLUE. The caller holds 0.
Diagrams#
Impact#
The session key spends 1000 GLUE from the wallet even though its limit is 100 GLUE. Because the validator is public, the caller does not have to be the wallet. The wallet loses the full 1000 GLUE.
Remediation#
Compare amount to sessionLimit inside _validateSingleCall and revert when the spend is above the limit. Making validateSessionKeyParams internal, which is the report's recommendation, stops an arbitrary caller from invoking it. Both are required: an internal function that still ignores the limit would overspend when the wallet itself calls it.
Sources#
- AuditVault finding 61407
- Shieldify Etherspot Credible Account Module review
- Reviewed module: etherspot-modular-accounts@d4774db
src/modules/validators/CredibleAccountModule.sol
Taxonomy#
severity/highvuln/access-control/missing-auth·vuln/logic/missing-check·vuln/access-control/missing-modifier
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 61407-c-05-in-credibleaccountmodule-the-validatesessionkeyparams-f_exp (evm-hack-registry mirror).
- AuditVault finding: 61407-c-05-in-credibleaccountmodule-the-validatesessionkeyparams-f.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.