Back to skill

Security audit

Mpp Mobula

Security checks for vulnerabilities and agentic risk

Overview

The skill discloses that it creates funded crypto hot wallets and auto-pays API calls, but its payment and private-key controls are too broad for low-friction installation.

Review before installing, especially if an agent can call this automatically or for multiple users. Keep only very small balances in the generated wallets, avoid exposing generic path calls to untrusted input, and prefer a version that validates request origin, endpoint prefix, currency, recipient, expiration, and cumulative spending before any transaction is signed.

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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
src/mpp/tempo-client.ts:166
Finding

Unrestricted request path permits attacker-controlled payment challenges and arbitrary recipients

Content
View full analysis
, privateKeyHex: Hex ): Promise { const url = new URL(`${MOBULA_BASE_URL}${path}`); Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); // First attempt — no auth const res1 = await fetch(url.toString()); if (res1.ok) return res1.json(); if (res1.status !== 402) { throw new Error(`Mobula ${path}: ${res1.status} ${await res1.text()}`); } // Parse 402 challenge const wwwAuth = res1.headers.get("www-authenticate"); if (!wwwAuth) throw new Error("402 but no WWW-Authenticate header"); const challenge = parseWwwAuthenticate(wwwAuth); if (!challenge) throw new Error(`Could not parse challenge from: ${wwwAuth}`); if (challenge.chainId !== 4217) { throw new Error(`Unexpected chain in challenge: ${challenge.chainId} (expected Tempo 4217)`); } const required = BigInt(challenge.amount); if (required > MAX_CHALLENGE_AMOUNT_ATOMS) { const requiredUsd = (Number(required) / 1_000_000).toFixed(4); const capUsd = (Number(MAX_CHALLENGE_AMOUNT_ATOMS) / 1_000_000).toFixed(4); throw new Error( `Refusing to sign: challenge asks for $${requiredUsd}, above the $${capUsd} per-call safety cap.`, ); } // SECURITY: only Mobula's documented payment recipient is accepted. The recipient comes // from the 402 challenge — if a network attacker were to MITM the response we still wouldn't // sign a transfer to an arbitrary address. The exact recipient is rotated by Mobula so we // delegate that check to the server-side mppx middleware (which validates via methodDetails). // Combined with the amount cap above ...[truncated 3710 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
src/wallet.ts:111
Finding

Public plaintext-key API does not enforce multi-tenant wallet authorization

Content
View full analysis
{ const path = walletPath(userId); if (!(await fileExists(path))) return null; const secret = await getSecret(); const key = deriveKey(secret, userId); const record = JSON.parse(await readFile(path, "utf8")); return decrypt(key, record.encryptedKey); } ``` The function is consumed using only a caller-supplied numeric identifier: ```ts export async function userMobulaCall( telegramUserId: number, path: string, params: Record = {} ): Promise { const privateKey = await getUserPrivateKey(telegramUserId); if (!privateKey) throw new Error("No wallet found. Create one first with /start."); return tempoFetch(path, params, privateKey as Hex); } ``` ### Technical Analysis HMAC-based derivation creates a different encryption key for each user ID, but it does not provide authorization. Every caller executing in the same process can access the shared master secret through this module and invoke the exported `getUserPrivateKey` function with an arbitrary user ID. There is no authenticated principal, ownership check, capability token, or access-control decision binding the calling user to the requested wallet. Consequently, the documented claim that different users have “no decryption path” is inaccurate at the application layer. Cryptographic separation of stored ciphertext does not prevent an authorized process from deriving all tenant keys. Returning the plaintext private key also gives every caller more privilege than is required for the declared operation. A caller that only needs to request a paid API ...[truncated 1223 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
src/cli-wallet.ts:56
Finding

Sensitive wallet files are created before restrictive permissions are applied

Content
View full analysis
{ await mkdir(CLI_WALLET_DIR, { recursive: true }); if (await exists(CLI_SECRET_FILE)) { const hex = (await readFile(CLI_SECRET_FILE, "utf8")).trim(); return Buffer.from(hex, "hex"); } const secret = randomBytes(32); await writeFile(CLI_SECRET_FILE, secret.toString("hex")); await chmod(CLI_SECRET_FILE, 0o600); return secret; } ``` ```ts await writeFile(CLI_WALLET_FILE, JSON.stringify(stored, null, 2)); await chmod(CLI_WALLET_FILE, 0o600); ``` The same pattern is used for per-user secrets and wallets: ```ts async function getSecret(): Promise { if (_secret) return _secret; if (await fileExists(SECRET_FILE)) { _secret = Buffer.from(await readFile(SECRET_FILE, "utf8"), "hex"); } else { _secret = randomBytes(32); await mkdir(join(SECRET_FILE, ".."), { recursive: true }); await writeFile(SECRET_FILE, _secret.toString("hex")); await chmod(SECRET_FILE, 0o600); } return _secret; } ``` ```ts await writeFile(path, JSON.stringify(record, null, 2)); await chmod(path, 0o600); ``` ### Technical Analysis The files are initially created using the process's default mode, modified by its umask, and only afterward restricted with `chmod(0600)`. With a common `0022` umask, a new regular file may temporarily be readable as `0644`. The separate existence check and write also form a time-of-check-to-time-of-use race. A local attacker may create or replace the predictable path between those operations, including with a symbolic link. The directory itself is created without an explicit `0700` mode. The encryption secret is especially sensitive because obtaining it together with the encrypted wallet file permits offline r ...[truncated 1157 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Note
Location
package.json:37
Finding

Security-sensitive dependency installation is not reproducibly pinned

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
Findings (19)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The description frames the skill as simple API access, but the content also creates cryptocurrency wallets, stores encrypted private keys, derives addresses, and performs paid on-chain transactions on behalf of users. That mismatch is dangerous because operators may approve or install the skill expecting read-only market-data access when it actually introduces custodial key management and financial-execution behavior.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
97% confidence
Finding

The skill’s stated purpose is API access and market-data fetching, but this file creates persistent user wallets and manages encrypted private keys on disk. That materially expands the trust boundary and attack surface: compromise of the host, secret file, or any code path exposing wallet operations could enable unauthorized spending from user-controlled funds, which is much more sensitive than read-only data access.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The exported getUserPrivateKey function returns a user’s raw decrypted private key as a string, enabling any caller with access to this function to fully exfiltrate and reuse wallet credentials outside intended flows. In the context of a pay-per-call crypto skill, that means direct loss of funds, unauthorized transactions, and irreversible compromise of wallet identity, making this especially dangerous.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
71% confidence
Finding

The README documents persistent local storage of a hot wallet at ~/.mpp-skill/wallet.json for later automated payments. While persistence is operationally necessary, it increases risk because a compromised host, weak filesystem protections, or accidental reuse in shared environments could expose a funded signing key and enable unauthorized spending.

Content

Scanner excerpt · README.md (reported line 14)May include surrounding context.

md
cd mpp-skill
bun install

# 1. Create a Tempo wallet (saved at ~/.mpp-skill/wallet.json, chmod 600)
bun run start wallet-create

# 2. Fund it with USDC.e via the bridge link printed by step 1

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The README explicitly states that the skill will decode a payment challenge, sign and broadcast an on-chain USDC.e transfer, and retry requests automatically, but it does not give a prominent warning that normal API calls spend user funds and create blockchain transactions. In an agent-skill context, this is more dangerous because downstream agent developers may integrate it as a data-fetching helper and unintentionally allow automated spending on behalf of users.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding

The skill declares no explicit tool scope or permission boundaries even though it clearly requires shell and network access to install packages, create wallets, bridge funds, call RPC endpoints, and contact external APIs. In an agent environment, missing scope declarations increases the chance that the agent can invoke broader capabilities than the user expects, which is risky because the skill also manages funds and private-key material.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
91% confidence
Finding

The skill intentionally persists wallet files and encryption secrets on disk so the agent can reuse a funded hot wallet across sessions. Persistent storage of private-key material is inherently sensitive: if the host, workspace, backups, or neighboring processes are compromised, an attacker may obtain the secret and drain funds or impersonate users on-chain.

Content

Scanner excerpt · SKILL.md (reported line 22)May include surrounding context.

md
cd mpp-skill
bun install

# 2. Create a hot wallet (AES-256-GCM encrypted at ~/.mpp-skill/wallet.json,
#    encryption key at ~/.mpp-skill/.secret, both chmod 600)
bun run start wallet-create
# → prints address + bridge link

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SKILL.md (reported line 87)May include surrounding context.

const price = await userMobulaCall(userId, "/api/2/token/price", { asset: "bitcoin" });

text

Per-user wallets are stored at `.claude/claudeclaw/wallets/{userId}.json`, AES-256-GCM encrypted with a key derived from `HMAC-SHA256(WALLET_SECRET, userId)`. The secret is auto-generated once at `.claude/claudeclaw/wallet.secret` (chmod 600). Different users cannot decrypt each other's keys.

### Direct low-level call

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 14)May include surrounding context.

md
*
 * SECURITY:
 *  - Private key is AES-256-GCM encrypted at rest.
 *  - Encryption key is a 32-byte secret stored at ~/.mpp-skill/.secret (chmod 600),
 *    auto-generated on first wallet creation.
 *  - The plaintext private key only exists in memory inside this process,
 *    just long enough to sign a Tempo transaction.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SECURITY.md (reported line 18)May include surrounding context.

md
*
 * SECURITY:
 *  - Private key is AES-256-GCM encrypted at rest.
 *  - Encryption key is a 32-byte secret stored at ~/.mpp-skill/.secret (chmod 600),
 *    auto-generated on first wallet creation.
 *  - The plaintext private key only exists in memory inside this process,
 *    just long enough to sign a Tempo transaction.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SECURITY.md (reported line 19)May include surrounding context.

md
*
 * SECURITY:
 *  - Private key is AES-256-GCM encrypted at rest.
 *  - Encryption key is a 32-byte secret stored at ~/.mpp-skill/.secret (chmod 600),
 *    auto-generated on first wallet creation.
 *  - The plaintext private key only exists in memory inside this process,
 *    just long enough to sign a Tempo transaction.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SKILL.md (reported line 23)May include surrounding context.

md
*
 * SECURITY:
 *  - Private key is AES-256-GCM encrypted at rest.
 *  - Encryption key is a 32-byte secret stored at ~/.mpp-skill/.secret (chmod 600),
 *    auto-generated on first wallet creation.
 *  - The plaintext private key only exists in memory inside this process,
 *    just long enough to sign a Tempo transaction.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · src/cli-wallet.ts (reported line 6)May include surrounding context.

ts
*
 * SECURITY:
 *  - Private key is AES-256-GCM encrypted at rest.
 *  - Encryption key is a 32-byte secret stored at ~/.mpp-skill/.secret (chmod 600),
 *    auto-generated on first wallet creation.
 *  - The plaintext private key only exists in memory inside this process,
 *    just long enough to sign a Tempo transaction.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The manifest description narrows the skill's purpose to fetching crypto prices, wallet positions, and market data. The call subcommand allows invoking any /api/2/* endpoint with arbitrary parameters, which expands behavior beyond the specifically described data-access scope.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The call command can trigger paid API requests using the user's wallet funds with arbitrary paths and parameters, yet it provides no per-call cost disclosure, dry-run, or confirmation prompt. In a CLI/agent context, this makes accidental or scripted wallet depletion easier because a user may invoke broad or repeated calls without realizing each request spends USDC.e.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This code can autonomously spend funds from a user-supplied private key in response to a remote 402 challenge, then retries the request with payment credentials, all without any explicit user confirmation at the point of spend. Although there is a per-call cap and chain check, a malicious or compromised server, repeated invocation by upstream code, or misuse of the function could still trigger unintended on-chain payments from a hot wallet.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
83% confidence
Finding

The runtime dependency viem is specified with a caret range (^2.48.4), allowing future compatible releases to be installed automatically. For a wallet-handling, networked skill that can sign or facilitate paid API calls, unpinned runtime dependencies increase supply-chain risk because a compromised or buggy upstream release could alter transaction, RPC, or signing behavior without code changes in this repository.

Content

Scanner excerpt · package.json (reported line 33)May include surrounding context.

json
"bun": ">=1.0.0"
  },
  "dependencies": {
    "viem": "^2.48.4"
  },
  "devDependencies": {
    "@types/bun": "latest"

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
70% confidence
Finding

Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Content

Scanner excerpt · package.json (reported line 36)May include surrounding context.

json
"viem": "^2.48.4"
  },
  "devDependencies": {
    "@types/bun": "latest"
  },
  "peerDependencies": {
    "bun": ">=1.0.0"

Unverifiable Dependency: bun has 2 known advisory(ies) (CVE-2025-8022 (bun vulnerable to OS Command Injection); CVE-2024-21548 (Bun has an Application-level Prototype Pollution vulnerability in the runtime na)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
90% confidence
Finding

The manifest allows any Bun version >=1.0.0 in both engines and peerDependencies, while known advisories exist for some Bun releases. Because this skill is intended to run with filesystem access, network access, and a hot wallet, failing to constrain the runtime version makes it impossible to verify whether deployments are exposed to Bun vulnerabilities such as command injection or prototype pollution.

Content

No source excerpt is available for this finding.