Back to skill

Security audit

Agent Wallet (Lobster)

Security checks for vulnerabilities and agentic risk

Overview

This is a real wallet/payment skill, but its payment signing path does not sufficiently constrain or disclose the exact transaction terms before signing.

Review this before installing or using it with real funds. Use a dedicated low-balance wallet, prefer testnet first, avoid arbitrary payment URLs, and do not rely on --confirm as proof that the amount, token contract, recipient, and expiration were safely reviewed. The payment code should add strict network/USDC contract checks, local spend limits, recipient validation, exact amount parsing, and informed confirmation of the resolved payment terms before signing.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
lib/x402-client.mjs:75
Finding
Untrusted x402 Server Controls Signed Payment Authorization Terms<![CDATA[ ## Vulnerability Details **File Location**: `lib/x402-client.mjs:75-81`, `lib/crypto.mjs:40-82`, `wallet.mjs:65-81` **Vulnerability Type**: Insufficient validation of server-supplied payment parameters **Risk Level**: High ### Complete Vulnerable Code Snippets From `lib/x402-client.mjs:75-81`: ```js // Find a supported payment option (Base USDC) const option = requirements.accepts?.[0]; if (!option || option.scheme !== 'exact') { throw new Error('No supported payment option (exact scheme on Base)'); } // Sign the payment authorization const payment = await createEIP3009Signature(privateKey, option, requirements.x402Version || 1); ``` From `lib/crypto.mjs:40-82`: ```js // Parse network from EIP-155 namespace (e.g., "eip155:8453") const chainId = parseInt(requirement.network.split(':')[1], 10); // Generate cryptographically secure random nonce (32 bytes) const nonceBytes = new Uint8Array(32); crypto.getRandomValues(nonceBytes); const nonce = `0x${Buffer.from(nonceBytes).toString('hex')}`; const now = Math.floor(Date.now() / 1000); const validAfter = BigInt(now - 60); const validBefore = BigInt(now + (requirement.maxTimeoutSeconds || requirement.requiredDeadlineSeconds || 300)); // Parse amount: decimal string ("5.00") or base units const maxAmount = requirement.maxAmountRequired; let value; if (typeof maxAmount === 'string' && maxAmount.includes('.')) { value = BigInt(Math.floor(parseFloat(maxAmount) * 1e6)); } else if (x402Version >= 2 || String(maxAmount).length > 6) { value = BigInt(maxAmount); } else { value = BigInt(maxAmount) * BigInt(1e6); } const authorization = { from: account.address, to: requirement.payTo || requirement.payToAddress, value, validAfter, validBefore, nonce, }; const domain = { name: requirement.extra?.name || 'USD Coin', version: requirement.extra?.version || '2', chainId, verifyingContract: requirement.asset || requirement.usdcAddress, }; const types = { TransferWithAuthorization: [ ...[truncated 3225 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define a strict local mapping of supported networks to canonical USDC contracts. 2. Require the server-supplied network to match the locally configured wallet network. 3. Require `requirement.asset` to exactly match the canonical USDC contract for that chain. 4. Validate all recipient addresses and optionally require an application-level recipient allowlist. 5. Introduce a mandatory local maximum payment amount that cannot be overridden by the server. 6. Reject missing, malformed, excessive, or negative authorization deadlines. 7. Retrieve and validate the payment requirements before asking for confirmation. 8. Display the exact chain, token contract, recipient, amount, and expiration to the user. 9. Require a second, informed confirmation over those resolved terms before signing. 10. Bind the authorization to the expected request origin where the protocol and application design permit it. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
lib/crypto.mjs:51
Finding
Ambiguous Amount Parsing Can Produce Excessive Payment Authorizations<![CDATA[ ## Vulnerability Details **File Location**: `lib/crypto.mjs:51-61`, `x402-client.mjs:74-84` **Vulnerability Type**: Unsafe monetary-unit parsing and numeric conversion **Risk Level**: High ### Complete Vulnerable Code Snippet From `lib/crypto.mjs:51-61`: ```js // Parse amount: decimal string ("5.00") or base units const maxAmount = requirement.maxAmountRequired; let value; if (typeof maxAmount === 'string' && maxAmount.includes('.')) { value = BigInt(Math.floor(parseFloat(maxAmount) * 1e6)); } else if (x402Version >= 2 || String(maxAmount).length > 6) { value = BigInt(maxAmount); } else { value = BigInt(maxAmount) * BigInt(1e6); } ``` The duplicate implementation in `x402-client.mjs:74-84` contains the same behavior: ```js // Parse amount const maxAmount = requirement.maxAmountRequired; let value; if (typeof maxAmount === 'string' && maxAmount.includes('.')) { value = BigInt(Math.floor(parseFloat(maxAmount) * 1e6)); } else if (x402Version >= 2 || String(maxAmount).length > 6) { value = BigInt(maxAmount); } else { value = BigInt(maxAmount) * BigInt(1e6); } ``` ### Technical Analysis The code infers whether a payment amount is denominated in whole USDC or base units using protocol version, the presence of a decimal point, and string length. Monetary units should not be inferred from the number of digits. For version 1 inputs with six or fewer digits and no decimal point, the implementation multiplies the supplied value by `1,000,000`. If such an input was already expressed in base units, the resulting authorization is inflated by a factor of one million. Decimal inputs are converted using `parseFloat`, which uses IEEE-754 floating-point arithmetic. This can introduce rounding and precision inconsistencies before conversion to `BigInt`. The parser also lacks strict canonical-format validation and a local maximum amount. ### Attack Path 1. A malicious or incompatible x402 server returns an amount whose unit representation is ambigu ...[truncated 996 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Implement the exact amount encoding required by each supported x402 protocol version. 2. Do not infer units from string length or the presence of a decimal point. 3. Prefer canonical, unsigned base-unit integer strings parsed directly with `BigInt`. 4. Reject decimal points, signs, exponent notation, whitespace, hexadecimal notation, empty values, and non-digit characters when base units are required. 5. If human-readable decimal input must be supported, use deterministic string-based fixed-point conversion rather than `parseFloat`. 6. Reject zero, negative, or unreasonably large amounts. 7. Enforce a locally configured maximum payment amount before signing. 8. Present the normalized amount and unit to the user before final confirmation. 9. Centralize amount parsing in one audited implementation to prevent the duplicate clients from diverging. 10. Add tests covering protocol versions, boundary values, six- and seven-digit inputs, precision cases, malformed values, and overflow-sized inputs. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
x402-client.mjs:141
Finding
Alternate x402 Client Signs Payments Without Its Claimed Confirmation Gate<![CDATA[ ## Vulnerability Details **File Location**: `x402-client.mjs:141-178` **Vulnerability Type**: Missing authorization check before signing a payment **Risk Level**: Medium ### Complete Vulnerable Code Snippet ```js /** * Fetch with automatic x402 payment handling. * Requires explicit --confirm flag. */ export async function x402Fetch(account, url, options = {}) { // Initial request const response = await fetch(url, options); if (response.status !== 402) { return response; } // Parse payment requirements const requirements = await parsePaymentRequired(response); if (!requirements) { throw new Error('Received HTTP 402 but could not parse payment requirements'); } // Find supported payment option const requirement = requirements.accepts.find( r => r.scheme === 'exact' && SUPPORTED_NETWORKS[r.network] ); if (!requirement) { throw new Error('No supported x402 payment options found for this endpoint'); } // Create and sign payment const payment = await createPaymentSignature(account, requirement, requirements.x402Version); // Retry with payment header const paidResponse = await fetch(url, { ...options, headers: { ...options.headers, 'X-Payment': encodePaymentHeader(payment), }, }); return paidResponse; } ``` ### Technical Analysis The function documentation states that an explicit confirmation flag is required, but `x402Fetch` never checks `options.confirm` or any equivalent authorization signal. Once the initial request receives an HTTP 402 response containing a supported network and `exact` payment scheme, the function signs automatically. This module is separate from `lib/x402-client.mjs`, which does check `options.confirm`. The duplicate implementations therefore expose inconsistent security behavior. An integrator importing the root-level module may reasonably rely on its documented guarantee and unintentionally enable automatic signing. Although this implement ...[truncated 1132 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add a mandatory confirmation check before making the initial request or processing any payment: ```js if (options.confirm !== true) { throw new Error('Explicit confirmation is required for x402 payments'); } ``` 2. Prefer a two-stage API that first returns validated payment terms and signs only through a separate explicit approval call. 3. Remove `confirm` from the options passed to `fetch`. 4. Validate that the requested asset is the canonical USDC contract for the selected supported network. 5. Apply recipient, amount, deadline, and local spending-limit checks before signing. 6. Consolidate the root and `lib/` x402 implementations into a single module with one security policy. 7. Add tests proving that missing, false, null, or malformed confirmation values cannot trigger signing. ]]>

T08 · Insecure Dependencies

Note
Location
package-lock.json:17
Finding
Cryptographic Dependencies Are Resolved Through a Non-Default Registry Mirror<![CDATA[ ## Vulnerability Details **File Location**: `package-lock.json:17-19` and additional `resolved` entries throughout the lockfile **Vulnerability Type**: Dependency supply-chain trust exposure **Risk Level**: Low ### Complete Relevant Code Snippet ```json "node_modules/@adraffy/ens-normalize": { "version": "1.11.1", "resolved": "https://registry.npmmirror.com/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" } ``` Other dependencies, including `viem` and its cryptographic dependency tree, are also resolved through `registry.npmmirror.com`. ### Technical Analysis The lockfile directs package installation to a non-default npm registry mirror. This creates an additional trust dependency for software that handles private-key derivation and EIP-712 signatures. The recorded Subresource Integrity hashes provide meaningful protection against arbitrary package substitution when package managers verify them correctly. Therefore, the mirror usage is not evidence that the dependencies are malicious. However, it increases supply-chain, provenance, and availability risk compared with using an explicitly approved registry. ### Attack Path 1. A user installs the project dependencies using the committed lockfile. 2. The package manager retrieves archives from the configured mirror. 3. A compromise, misconfiguration, or provenance inconsistency at that distribution intermediary could affect dependency delivery. 4. If integrity verification were bypassed, weakened, or paired with a maliciously modified lockfile, altered dependency code could execute in the wallet process. 5. Since `viem` and related libraries handle private keys and signatures in memory, compromised dependency code could potentially access signing material or alter authorizations. ### Impact Assessment No malicious package behavior or integrity mismatch was ...[truncated 465 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Regenerate the lockfile using the organization's explicitly trusted npm registry. 2. Document the approved registry and enforce it through project or CI configuration. 3. Pin security-sensitive dependencies to reviewed exact versions rather than relying only on compatible version ranges in `package.json`. 4. Preserve and verify package integrity hashes during installation. 5. Use reproducible, frozen-lockfile installation in CI and deployment environments. 6. Monitor `viem` and its transitive cryptographic dependencies for security advisories. 7. Require review of lockfile changes, especially changes to `resolved`, `integrity`, package names, or cryptographic dependency versions. 8. Consider provenance verification or an internal package proxy for security-sensitive wallet deployments. ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (8)

Known Vulnerable Dependency: ws==8.20.1 — 1 advisory(ies): CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
93% confidence
Finding
The lockfile pins ws to version 8.20.1, and the provided advisory indicates this version is affected by a memory-exhaustion denial-of-service issue triggered by tiny fragmented frames/data chunks. Even though this is a transitive dependency pulled in via viem, a lockfile-pinned vulnerable package is still a real supply-chain exposure if the application uses WebSocket functionality or processes untrusted network traffic.

Credential Access

High
Category
Privilege Escalation
Content
if (!pk) {
    throw new Error(
      'x402 payments require WALLET_PRIVATE_KEY.\n' +
      'Set it in .env or as an environment variable.'
    );
  }
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The module header says 'Phase 1: Read-only balance queries (safe, no signing)' and frames payment behavior as a later phase. However, getWallet already supports a private-key-based 'full mode' by reading WALLET_PRIVATE_KEY and returning it in the wallet object, which contradicts the documented phase boundary and safety framing.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The function accepts an x402Version parameter and uses it when interpreting the amount, but the returned object always hard-codes x402Version: 1. This can create a version/signature semantic mismatch where downstream consumers validate or process the payload under the wrong protocol version, potentially causing incorrect payment handling, rejected transactions, or signing data under assumptions different from what verifiers enforce.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The implementation contradicts its own safety contract: despite the docstring stating payment handling requires explicit confirmation, x402Fetch automatically signs a payment authorization and retries the request on any 402 response. This lets any contacted endpoint trigger a wallet signature for a payment payload without an execution-time consent gate, which is especially dangerous because the signed authorization can authorize a USDC transfer if later submitted on-chain.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
On a 402 response, the code automatically replays the original request with an added X-Payment header, preserving the original options and body. This can resend sensitive request data or repeat non-idempotent actions to the same endpoint without an explicit warning, compounding privacy and transaction risks when interacting with untrusted or malicious servers.

Intent-Code Divergence

Low
Confidence
95% confidence
Finding
The JSDoc for payX402 declares `@returns {Promise<Response>}`, which implies callers receive a Response object directly. In reality, both return paths yield an object containing `paid`, `status`, and `response`, so the documentation contradicts the actual interface and intent of the function.

Unpinned Dependencies

Low
Category
Supply Chain
Content
],
  "license": "MIT-0",
  "dependencies": {
    "viem": "^2.50.4"
  }
}
Confidence
92% confidence
Finding
The dependency uses a caret range (^2.50.4), which permits automatic installation of newer minor and patch releases. In a wallet/payment-related skill, this increases supply-chain risk because an unexpected upstream change or compromised release of a blockchain library could alter signing, transaction handling, or payment behavior without explicit review.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
lib/core.mjs:64