Back to skill

Security audit

Use User Controlled Wallets

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherent wallet-integration guidance, but its reusable examples expose sensitive wallet credentials and leave important authorization and transaction controls under-specified.

Review before installing or using in production. The skill may be useful for Circle wallet prototypes, but do not copy the storage, token-minting, or transfer examples as-is. Require authenticated server-side sessions, derive wallet user identity on the backend, keep wallet credentials out of localStorage and JavaScript-readable cookies, validate and authorize every transaction parameter server-side, default to testnet, and pin reviewed dependency versions.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
Findings (6)

T09 · Insecure Skill Coding Practices

Error
Location
references/create-wallet-pin.md:201
Finding
Wallet credentials persisted in browser localStorage in the PIN workflow<![CDATA[ ## Vulnerability Details **File Location**: `references/create-wallet-pin.md:201-209` **Vulnerability Type**: Browser-accessible storage of sensitive authentication material **Risk Level**: High ### Vulnerable Code ```tsx const handleGetUserToken = async () => { if (!userId) return; const response = await fetch(`${apiBaseUrl}/api/wallet/get-token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId }), }); const data = await response.json(); setCredentials({ userToken: data.userToken, encryptionKey: data.encryptionKey }); localStorage.setItem("userToken", data.userToken); localStorage.setItem("encryptionKey", data.encryptionKey); }; ``` ### Technical Analysis The example persists both `userToken` and `encryptionKey` in `localStorage`. Any JavaScript running under the application's origin can read these values. This includes injected scripts resulting from cross-site scripting, compromised third-party frontend packages, malicious analytics scripts, and potentially hostile browser extensions. These credentials are subsequently passed to `sdk.setAuthentication()` and are therefore security-sensitive wallet authorization material. Although `SKILL.md` warns that `localStorage` should not be used in production, the reference presents this behavior as part of a directly reusable implementation. ### Attack Path 1. A user authenticates through the PIN workflow. 2. The application saves the Circle `userToken` and `encryptionKey` in `localStorage`. 3. An attacker obtains same-origin script execution through an XSS vulnerability or compromised frontend dependency. 4. The attacker reads: ```js localStorage.getItem("userToken"); localStorage.getItem("encryptionKey"); ``` 5. The attacker exfiltrates the credentials and attempts to initialize the wallet SDK or execute available wallet challenges before the credentials expire. ### Impact Assessment Successful exploitation exp ...[truncated 377 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove `userToken` and `encryptionKey` persistence from `localStorage`. - Replace the example with a production-safe design rather than relying only on a warning. - Use a short-lived, server-managed session represented by an opaque cookie configured with `HttpOnly`, `Secure`, and an appropriate `SameSite` policy. - Ensure sensitive Circle credentials are never returned to unnecessary frontend components. - If client-side access to a credential is unavoidable for SDK operation, keep it only in memory, minimize its lifetime, clear it on logout or inactivity, and deploy a strict Content Security Policy. - Audit all frontend dependencies and eliminate XSS sinks because an `HttpOnly` session alone does not prevent an injected script from issuing authenticated actions. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
references/create-wallet-email-otp.md:151
Finding
Wallet credentials persisted in browser localStorage in the email OTP workflow<![CDATA[ ## Vulnerability Details **File Location**: `references/create-wallet-email-otp.md:151-159` **Vulnerability Type**: Browser-accessible storage of sensitive authentication material **Risk Level**: High ### Vulnerable Code ```tsx const onLoginComplete = (error: unknown, result: unknown) => { if (error) { setStatus("Login failed: " + ((error as Error).message || "Unknown error")); return; } const loginRes = result as LoginResult; setLoginResult(loginRes); localStorage.setItem("userToken", loginRes.userToken); localStorage.setItem("encryptionKey", loginRes.encryptionKey); setStatus("Email verified. Ready to initialize user."); }; ``` ### Technical Analysis After successful OTP verification, the code writes the resulting `userToken` and `encryptionKey` to origin-wide browser storage. `localStorage` has no `HttpOnly` protection and remains available across page reloads until explicitly removed. The example does not implement logout cleanup for these values. The OTP verifies the user at login time, but persistent exposure of the resulting credentials weakens the security gained from that verification. A later same-origin script compromise can retrieve the authenticated session material without repeating the OTP process. ### Attack Path 1. The victim completes email OTP verification. 2. The callback stores the resulting wallet credentials in `localStorage`. 3. Before the credentials are removed or expire, attacker-controlled JavaScript executes in the application origin. 4. The script reads and transmits the stored `userToken` and `encryptionKey`. 5. The attacker attempts to replay the credentials against wallet SDK or backend operations. ### Impact Assessment An attacker may obtain the authenticated wallet session material of users who completed OTP verification. Potential consequences include disclosure of wallet metadata and balances, creation of wallet-operation challenges, or attempts to execute sensitive operations within t ...[truncated 66 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not store `userToken` or `encryptionKey` in `localStorage` or `sessionStorage`. - Use short-lived server-side sessions and backend-set `HttpOnly`, `Secure`, appropriately scoped `SameSite` cookies. - If SDK constraints require browser access to particular material, retain it only in memory and require reauthentication after reload. - Explicitly clear all authentication state on logout, timeout, authentication failure, and account changes. - Deploy a restrictive Content Security Policy and sanitize all user-controlled content to reduce XSS risk. - Never include authentication or encryption material in logs, URLs, analytics, or error-reporting payloads. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
references/create-wallet-social-login.md:143
Finding
Social-login wallet credentials stored in JavaScript-readable cookies<![CDATA[ ## Vulnerability Details **File Location**: `references/create-wallet-social-login.md:143-152` **Vulnerability Type**: Insecure client-side cookie storage of wallet credentials **Risk Level**: High ### Vulnerable Code ```tsx const onLoginComplete = (error: unknown, result: unknown) => { if (cancelled) return; if (error) { setStatus("Login failed: " + ((error as Error).message || "Unknown error")); return; } const { userToken, encryptionKey } = result as { userToken: string; encryptionKey: string }; setCookie("userToken", userToken); setCookie("encryptionKey", encryptionKey); }; ``` The same workflow also stores device credentials without explicit security attributes: ```tsx setCookie("deviceToken", data.deviceToken); setCookie("deviceEncryptionKey", data.deviceEncryptionKey); ``` ### Technical Analysis Cookies created by frontend JavaScript cannot be marked `HttpOnly`. Consequently, any JavaScript executing in the origin can read the wallet session and encryption credentials. The calls also do not specify `Secure`, `SameSite`, path, domain, or expiration controls. This directly conflicts with the security rule in `SKILL.md` that recommends `HttpOnly` cookies. `react-cookie` is suitable for browser-managed state, but it cannot safely create an `HttpOnly` credential cookie because that attribute must be set by the server. ### Attack Path 1. The victim completes the social OAuth flow. 2. The login callback writes wallet credentials to JavaScript-readable cookies. 3. An attacker exploits XSS or compromises a frontend dependency. 4. The attacker reads `document.cookie` or uses the cookie library to retrieve the credentials. 5. The stolen credentials are replayed while valid to attempt wallet access or challenge execution. If the application is served without enforced HTTPS and cookies lack the `Secure` attribute, transport exposure may also be possible. ### Impact Assessment The exposed data includes user authentication and e ...[truncated 296 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not store wallet secrets in cookies created by frontend JavaScript. - Exchange login results for an opaque application session through a trusted backend. - Have the backend issue cookies with `HttpOnly`, `Secure`, a narrowly scoped `Path`, short `Max-Age`, and `SameSite=Strict` or a justified `SameSite=Lax` setting. - Rotate session identifiers after login and invalidate them during logout. - Add CSRF protection to state-changing routes when cookie authentication is used. - Keep device encryption material in memory where SDK requirements permit, and minimize its lifetime and exposure. - Enforce HTTPS and HSTS in production. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
references/create-wallet-pin.md:65
Finding
Caller-controlled user identity can be exchanged for wallet session credentials without demonstrated authorization<![CDATA[ ## Vulnerability Details **File Location**: `references/create-wallet-pin.md:65-76` **Vulnerability Type**: Missing application-level authentication and identity binding **Risk Level**: Critical ### Vulnerable Code ```typescript /** * Gets a session token for a user. * Returns userToken (valid for 60 minutes) and encryptionKey. * Endpoint: POST /api/wallet/get-token { userId } */ export async function getUserToken(userId: string) { const response = await circleClient.createUserToken({ userId }); return { userToken: response.data?.userToken, encryptionKey: response.data?.encryptionKey, }; } ``` The corresponding frontend sends a user-entered identifier directly: ```tsx const response = await fetch(`${apiBaseUrl}/api/wallet/get-token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId }), }); ``` ### Technical Analysis The documented endpoint accepts a caller-controlled `userId` and exchanges it for a Circle `userToken` and `encryptionKey`. The reference does not demonstrate application authentication, authorization, rate limiting, or binding between the supplied Circle user ID and the currently authenticated application principal. If implemented literally, possession or discovery of another user's identifier may be sufficient to request that user's session credentials. The Circle API key remains server-side, but the backend effectively exposes a privileged credential-minting operation without a demonstrated access-control boundary. ### Attack Path 1. The attacker identifies or guesses a victim's Circle user ID, such as an email address, username, or predictable UUID. 2. The attacker submits: ```http POST /api/wallet/get-token Content-Type: application/json {"userId":"victim-user-id"} ``` 3. The backend calls `createUserToken` using its privileged Circle API key. 4. The backend returns the victim-associated `userToken` and `encryptionKey`. 5. The attacker uses ...[truncated 566 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require application authentication on every wallet endpoint. - Never accept the authoritative Circle user ID from the request body. - Derive the Circle user ID from the authenticated server-side application session. - Verify that every requested wallet and transaction belongs to that principal. - Return an opaque application session where possible instead of exposing raw provider credentials. - Apply strict authorization middleware, audit logging, per-user and per-IP rate limits, replay protection, and generic error responses. - Add CSRF protection when browser cookies authenticate the route. - Include negative authorization tests covering attempts to request tokens for another user. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
references/send-transaction.md:102
Finding
Transaction helper passes unvalidated transfer parameters to the wallet API<![CDATA[ ## Vulnerability Details **File Location**: `references/send-transaction.md:102-127` **Vulnerability Type**: Missing server-side validation and transaction-policy enforcement **Risk Level**: High ### Vulnerable Code ```typescript export async function createTransferByBlockchain( userToken: string, walletId: string, destinationAddress: string, amount: string, blockchain: string, tokenAddress: string = "", feeLevel: "LOW" | "MEDIUM" | "HIGH" = "MEDIUM" ) { const params: CreateTransactionInput = { userToken, walletId, destinationAddress, amounts: [amount], blockchain: blockchain as TokenBlockchain, tokenAddress, fee: { type: "level", config: { feeLevel } }, }; const response = await circleClient.createTransaction(params); return { challengeId: response.data?.challengeId }; } ``` ### Technical Analysis The helper passes caller-supplied wallet, destination, amount, blockchain, and token values directly to `createTransaction`. The expression `blockchain as TokenBlockchain` is only a compile-time TypeScript assertion and does not validate runtime input. The implementation does not demonstrate: - Chain-specific destination-address validation - Positive, bounded, canonical decimal amount validation - Wallet ownership checks - Token and blockchain allowlists - Verification that the token belongs to the selected blockchain - Balance and fee checks - Mainnet restrictions or high-value thresholds - Binding explicit user confirmation to the exact submitted parameters Although Circle's hosted challenge provides an additional authorization step, backend validation remains necessary to prevent parameter manipulation and misleading or unintended challenges. ### Attack Path 1. The attacker authenticates legitimately or compromises a frontend session. 2. The attacker bypasses the UI and directly calls the transfer endpoint. 3. The request supplies an attacker-controlled destination, excessive or malformed amount, ...[truncated 858 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Validate every transfer field on the backend with a strict schema. - Use chain-aware libraries to validate and normalize destination and token addresses. - Parse amounts with fixed-precision decimal logic; reject zero, negative, noncanonical, excessive, or over-precision values. - Derive wallet ownership from the authenticated server-side principal. - Allowlist supported networks and trusted token identifiers or contracts. - Verify balances, anticipated fees, token/network consistency, and configured transaction limits. - Default to testnet and require an additional explicit confirmation for mainnet or high-value transfers. - Present destination, amount, token, network, and fees on a final confirmation screen. - Bind the confirmed parameters to a short-lived server-side transaction intent and reject any changed values. - Rate-limit challenge creation and record security audit events. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:18
Finding
Mutable latest dependency versions undermine reproducible security review<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:18` **Vulnerability Type**: Unpinned third-party dependencies **Risk Level**: Medium ### Vulnerable Code ```bash npm install @circle-fin/user-controlled-wallets@latest @circle-fin/w3s-pw-web-sdk@latest vite-plugin-node-polyfills ``` The Skill also states: ```markdown - ALWAYS install latest packages (`@circle-fin/user-controlled-wallets@latest`, `@circle-fin/w3s-pw-web-sdk@latest`) and `vite-plugin-node-polyfills` ``` ### Technical Analysis The installation command resolves mutable package versions at installation time. Therefore, the code installed by a developer may differ from the code that was available when the Skill was reviewed. `vite-plugin-node-polyfills` is also unpinned. No evidence indicates that these named packages are currently malicious. The issue is that mutable dependency resolution increases exposure to compromised upstream releases, accidental breaking changes, malicious maintainer updates, and unexpected transitive dependency changes. Wallet software has a particularly sensitive threat model because frontend dependencies may access wallet credentials and transaction data. ### Attack Path 1. An upstream package account, release process, or transitive dependency is compromised, or a new release introduces a security regression. 2. The attacker or compromised maintainer publishes the affected version under the existing package name. 3. A developer follows the Skill and installs `@latest` or another unpinned package. 4. npm resolves the new, unreviewed code. 5. Package installation, build, or runtime code executes in the developer or application environment. 6. The compromised code may access environment data, browser wallet credentials, transaction details, or build artifacts according to the privileges of the affected process. ### Impact Assessment The possible impact is bounded by the privileges of npm lifecycle scripts, the build process, and the deployed frontend. In ...[truncated 285 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin all direct dependencies to exact versions that have been reviewed. - Commit and review the package-manager lockfile. - Use reproducible clean installation commands such as `npm ci`. - Review dependency diffs and release notes before upgrades. - Enable automated vulnerability and provenance scanning. - Restrict or disable unnecessary npm lifecycle scripts in CI and development environments. - Use trusted registries, package integrity verification, and controlled dependency-update automation. - Retest wallet authentication and transaction security whenever SDK versions change. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (10)

Credential Access

High
Category
Privilege Escalation
Content
## Prerequisites

1. **Circle Developer Console**:
   - Get API key from Project Settings
   - Navigate to Wallets -> User Controlled -> Configurator
   - Copy the App ID from the configurator
   - No additional configuration needed -- PIN works out of the box
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## Prerequisites

1. **Circle Developer Console**:
   - Get API key from Project Settings
   - Navigate to Wallets -> User Controlled -> Configurator
   - Copy the App ID from the configurator
   - No additional configuration needed -- PIN works out of the box
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## Prerequisites

1. **Circle Developer Console**:
   - Get API key from Project Settings
   - Navigate to Wallets -> User Controlled -> Configurator
   - Copy the App ID from the configurator
   - No additional configuration needed -- PIN works out of the box
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Vague Triggers

Medium
Confidence
86% confidence
Finding
The trigger list includes broad terms like 'userToken', 'deviceToken', 'challenge execution', and generic wallet/auth phrases that could cause the skill to activate for loosely related requests. In a wallet and authentication context, over-broad matching increases the chance the agent applies sensitive wallet guidance in the wrong context, which can lead to unsafe implementation advice or inappropriate handling of transaction/authentication flows.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
- NEVER hardcode, commit, or log secrets (API keys, encryption keys). ALWAYS use environment variables or a secrets manager. Add `.gitignore` entries for `.env*` and secret files when scaffolding.
- ALWAYS implement both backend and frontend. The API key MUST stay server-side -- frontend-only builds would expose it.
- ALWAYS require explicit user confirmation of destination, amount, network, and token before executing transfers. NEVER auto-execute fund movements on mainnet.
- ALWAYS warn when targeting mainnet or exceeding safety thresholds (e.g., >100 USDC).
- ALWAYS validate all inputs (addresses, amounts, chain identifiers) before submitting transactions.
- ALWAYS warn before interacting with unaudited or unknown contracts.
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The frontend stores `userToken` and `encryptionKey` in `localStorage`, which is readable by any JavaScript running in the origin, including injected scripts from an XSS vulnerability or compromised third-party dependencies. In this wallet context, those values are authentication material for challenge execution, so theft could enable unauthorized wallet actions or account takeover within the SDK session model.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
The frontend example stores `userToken` and `encryptionKey` in `localStorage`, which is readable by any JavaScript running in the page origin, including injected script from XSS or compromised third-party dependencies. Because these values are used to authenticate wallet operations in the Web SDK, exposure can enable unauthorized wallet actions during the token lifetime.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
Sensitive wallet credentials are written to browser `localStorage` without any warning or justification, normalizing an unsafe storage pattern for developers who may copy the example verbatim. In a wallet context, this is especially dangerous because the stored values directly support authenticated challenge execution.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
The example sends `userId` and later `userToken` to backend API endpoints as part of wallet creation and initialization flows. While this is functionally expected, the markdown does not disclose to users that personally identifying and authentication data will be transmitted between frontend, backend, and Circle services.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The example explicitly persists highly sensitive wallet authentication material (`deviceToken`, `deviceEncryptionKey`, `userToken`, and `encryptionKey`) in browser cookies to survive OAuth redirects. If those cookies are accessible to client-side JavaScript or are not strictly protected with secure attributes, any XSS, browser extension, shared-device access, or misconfiguration could expose material sufficient to impersonate the user or authorize wallet operations. In a non-custodial wallet context, storing key-related secrets in cookies is especially dangerous because compromise can directly lead to unauthorized wallet access or transaction approval flows.

Static analysis

No suspicious patterns detected.