Fix: WalletConnect wc_sessionAuthenticate CACAO Signature Bypass
With the widespread adoption of WalletConnect v2 One-Click Auth (implementing CAIP-74 and EIP-4361 Sign-In with Ethereum), dApps can combine session establishment and wallet identity verification into a single QR-code scan or signature prompt.
However, formal protocol analysis and security audits have uncovered a critical vulnerability in the handling of wc_sessionAuthenticate: when a peer wallet transmits an invalid, expired, or corrupted signature inside the CACAO (Chain-Agnostic Authenticated Content Object) payload, client SDKs frequently persist the session in storage and return an active session object. Furthermore, valid signatures are not strictly cross-referenced against the dApp’s original challenge parameters (such as domain, nonce, and aud).
If your application is also securing iframe communication against unverified cross-window messages, review our guide on WalletConnect postMessage Listener Missing event.origin Validation.
Cryptographic Mechanics: CAIP-74 and the CACAO Structure
A CACAO token encapsulates a standard EIP-4361 SIWE message into a structured format designed for multi-chain relaying:
CACAO Object Structure:
├── h: Header -> { t: "eip4361" }
├── p: Payload -> { iss, aud, domain, nonce, iat, exp, statement, resources }
└── s: Sig -> { t: "eip191" | "eip1271", s: "<hex_signature>" }
The Vulnerability Vectors
Formal verification using protocol modeling engines (such as Verifpal) revealed two fundamental implementation weaknesses:
- Failure to Reject Invalid Signatures: If
verifySignature(cacao)throws an exception or evaluates tofalse, older SDK controller handlers catch the error internally, emit a warning log, yet continue executing the session persistence routine:// Vulnerable internal pattern in SignClient Auth Controller const isValid = await this.verifyCacao(authResponse.cacao).catch(() => false); // VULNERABILITY: If isValid is false, the session is STILL constructed! const session = this.createSessionFromAuth(authResponse); return { session, cacao: authResponse.cacao }; - Missing Request Parameter Binding: The wallet signs a CACAO payload generated on the wallet’s device. If the wallet tampers with the
domain, swaps thenonce, or targets a differentchainId, the dApp client fails to verify thatresponse.cacao.p.nonce === request.nonce, opening doors to replay and cross-dApp impersonation attacks.
Step-by-Step Remediation Protocol
To ensure that only cryptographically authentic, untampered sessions are authorized in your production dApp or node backend, enforce the following defensive pipeline.
1. Enforce Strict Server-Side SIWE / CACAO Validation
Never rely exclusively on client-side SDK verification flags. Validate the CACAO envelope on your API backend before issuing JSON Web Tokens (JWT) or granting authenticated access:
// server/auth/verifyCacao.ts
import { verifySiweMessage } from 'viem/siwe';
import { formatSiweMessage } from './cacaoUtils';
interface CacaoPayload {
h: { t: string };
p: {
iss: string; // "did:pkh:eip155:1:0x1234..."
domain: string;
aud: string;
nonce: string;
iat: string;
};
s: { t: string; s: string };
}
export async function validateCacaoSession(
cacao: CacaoPayload,
expectedNonce: string,
expectedDomain: string
): Promise<boolean> {
// 1. Nonce Matching Check
if (cacao.p.nonce !== expectedNonce) {
throw new Error('AUTH_REJECTED: CACAO nonce mismatch or expired.');
}
// 2. Domain Binding Check
if (cacao.p.domain !== expectedDomain) {
throw new Error('AUTH_REJECTED: CACAO domain does not match application origin.');
}
// 3. Extract Ethereum Address from DID PKH
const parts = cacao.p.iss.split(':');
const address = parts[4] as `0x${string}`;
// 4. Reconstruct Canonical EIP-4361 Message String
const reconstructedMessage = formatSiweMessage({
domain: cacao.p.domain,
address,
statement: cacao.p.statement,
uri: cacao.p.aud,
version: '1',
chainId: parseInt(parts[2], 10),
nonce: cacao.p.nonce,
issuedAt: cacao.p.iat,
});
// 5. Cryptographic Signature Verification (supports EIP-191 & EIP-1271 Smart Accounts)
const isValid = await verifySiweMessage({
message: reconstructedMessage,
signature: cacao.s.s as `0x${string}`,
domain: expectedDomain,
nonce: expectedNonce,
});
if (!isValid) {
throw new Error('AUTH_REJECTED: Cryptographic signature validation failed for CACAO.');
}
return true;
}
2. Client-Side Defensive Interceptor for signClient
In your frontend application or automated agent script, wrap signClient.authenticate() with explicit assertion gates:
// src/auth/safeAuthenticate.ts
import { SignClient } from '@walletconnect/sign-client';
export async function safeSessionAuthenticate(
signClient: InstanceType<typeof SignClient>,
authParams: any
) {
const generatedNonce = authParams.nonce;
// Initiate authentication handshake
const authResponse = await signClient.authenticate(authParams);
const { cacao } = authResponse;
if (!cacao || !cacao.s || !cacao.s.s) {
await signClient.disconnect({
topic: authResponse.session?.topic || '',
reason: { code: 4001, message: 'Missing CACAO signature' }
});
throw new Error('Authentication rejected: Wallet did not provide a signature.');
}
// Validate nonce binding
if (cacao.p.nonce !== generatedNonce) {
// Teardown the fraudulently initialized session immediately
if (authResponse.session) {
await signClient.disconnect({
topic: authResponse.session.topic,
reason: { code: 4001, message: 'Nonce mismatch' }
});
}
throw new Error('CRITICAL SECURITY: Received CACAO nonce does not match generated challenge!');
}
return authResponse;
}
Security Verification Matrix
| Vulnerability Check | Attack / Test Scenario | Expected Defensive Behavior |
|---|---|---|
| Invalid Signature String | Wallet returns 0x0000... dummy signature | SDK immediately purges topic; promise rejects; zero session persisted in storage. |
| Replay Old CACAO | Re-submit CACAO from previous session | Nonce verification fails; server returns 401 Unauthorized. |
| Tampered Domain | CACAO signed for evil.com instead of your dApp | Domain validation check aborts handshake before session establishment. |
| EIP-1271 Contract Wallet | Smart Account (Safe, Biconomy) signs CACAO | verifySiweMessage queries contract isValidSignature on-chain; validates successfully. |
Frequently Asked Questions
Q: Why was this bug present in the core WalletConnect protocol?
In the initial transition from standard pairing (wc_sessionPropose) to unified authentication (wc_sessionAuthenticate), session management code was optimized for low-latency UX. The assumption was that application backends would always perform independent signature validation, but many dApps relied entirely on the SDK’s local return values.
Q: Does this vulnerability affect standard WalletConnect v2 pairings (eth_requestAccounts)?
No. Standard pairings establish an encrypted communication channel first and request signatures via independent RPC calls (personal_sign / eth_signTypedData). The vulnerability is isolated to the wc_sessionAuthenticate (One-Click Auth) method implementing CAIP-74.
Q: Can smart contract wallets (ERC-4337 / Gnosis Safe) use CACAO authentication?
Yes. CACAO supports s.t: "eip1271". The verifier must execute an on-chain isValidSignature(bytes32 hash, bytes signature) call against the account contract address rather than recovering the public key with standard ecrecover.