Back to skill

Security audit

pulseai-skill

Security checks for vulnerabilities and agentic risk

Overview

This is a real on-chain marketplace skill, but it exposes wallet private keys and handles money-affecting actions with insufficient safeguards.

Review before installing. Do not run wallet generation in JSON mode unless you are prepared for the private key to appear in logs or transcripts, protect or rotate any key stored in ~/.pulse/config.json, and treat all buy, settle, cancel, operator, and provider-runtime commands as real mainnet actions that can spend funds or change on-chain state. Avoid untrusted offerings with remote schema URLs until schema fetching is restricted.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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 (2)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
src/commands/job.ts:544
Finding
Attacker-Controlled Schema URI Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `src/commands/job.ts:49-53`, `src/commands/job.ts:544-566` **Vulnerability Type**: Server-Side Request Forgery through an untrusted marketplace schema URI **Risk Level**: High ### Vulnerable Code ```ts const schema = await resolveRequirementsSchema( Number(offering.serviceType), offering.requirementsSchemaURI, ); ``` ```ts async function loadOfferingSchemaFromUri(schemaUri: string): Promise<OfferingSchema | null> { let schemaPayload: unknown; if (schemaUri.startsWith('data:')) { schemaPayload = parseDataUriJson(schemaUri); } else if (schemaUri.startsWith('http://') || schemaUri.startsWith('https://')) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30_000); try { const response = await fetch(schemaUri, { signal: controller.signal }); if (!response.ok) { throw new Error(`Failed to fetch requirements schema URI (${response.status} ${response.statusText})`); } const buf = await response.arrayBuffer(); if (buf.byteLength > 1_048_576) { throw new Error(`Schema too large (${buf.byteLength} bytes). Limit is 1MB`); } schemaPayload = JSON.parse(new TextDecoder().decode(buf)); } finally { clearTimeout(timeoutId); } } else if (schemaUri.trim().startsWith('{')) { schemaPayload = JSON.parse(schemaUri); } else { return null; } if (!isOfferingSchema(schemaPayload)) { throw new Error('requirementsSchemaURI did not resolve to a valid OfferingSchema document'); } return schemaPayload; } ``` ### Technical Analysis The `requirementsSchemaURI` value comes from marketplace offering data and is therefore controlled by the offering publisher. When a buyer creates a job with requirements, the CLI passes that value directly to `fetch()`. The implementation only verifies that the URI starts with `http://` or `https://`. It does not: - Reject loopback, privat ...[truncated 2874 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only `https:` schema URLs unless plaintext HTTP is explicitly required by a narrowly controlled development mode. 2. Prefer an allowlist of trusted schema-hosting domains. 3. Before connecting, resolve the hostname and reject every address in loopback, private, link-local, multicast, carrier-grade NAT, documentation, reserved, and cloud metadata ranges for both IPv4 and IPv6. 4. Disable automatic redirects with `redirect: 'manual'`, or validate the protocol, hostname, and resolved address before following every redirect. 5. Defend against DNS rebinding by ensuring that the address used for the actual connection is the validated address. 6. Explicitly block common metadata destinations, including `169.254.169.254` and their IPv6 equivalents. 7. Stream response data and abort as soon as the one-megabyte limit is exceeded rather than calling `arrayBuffer()` first. 8. Apply separate connection, header, and body timeouts. 9. Consider retrieving schemas through a hardened backend proxy with network egress restrictions instead of fetching them directly from the user's machine. 10. Treat remote schemas as untrusted data and retain strict structural and semantic validation after retrieval. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/commands/wallet.ts:63
Finding
Wallet Private Keys Are Printed to Standard Output and Stored Without Explicit Restrictive Permissions<![CDATA[ ## Vulnerability Details **File Location**: `src/commands/wallet.ts:63-88`, `src/config.ts:44-48`, `SKILL.md:115-116` **Vulnerability Type**: Plaintext sensitive-data exposure and insecure secret-file permissions **Risk Level**: High ### Vulnerable Code ```ts const config = loadConfig(); const existingKey = config.privateKey; if (existingKey) { const existingAddress = privateKeyToAccount(existingKey).address; info('Wallet key already exists at ~/.pulse/config.json'); output({ address: existingAddress, ...(isJsonMode() ? { privateKey: existingKey } : {}), message: OPERATOR_MESSAGE, }); success('Using existing wallet key.'); return; } info('Generating new wallet key...'); const privateKey = generatePrivateKey(); const account = privateKeyToAccount(privateKey); saveConfig({ privateKey }); success('Saved wallet key to ~/.pulse/config.json'); output({ address: account.address, ...(isJsonMode() ? { privateKey } : {}), message: OPERATOR_MESSAGE, }); ``` ```ts export function saveConfig(config: PulseConfig): void { try { fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true }); fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8'); } catch (e) { throw new Error( `Failed to save Pulse config to ${CONFIG_PATH}: ${e instanceof Error ? e.message : String(e)}`, ); } } ``` The skill instructions encourage the output mode that discloses the key: ```md ## Decision Guidelines - **Always use `--json`** for all commands — parse the JSON output for structured data ``` ### Technical Analysis The `wallet generate` command deliberately includes the complete private key in JSON output. This occurs both when generating a new wallet and when an existing wallet is found. As a result, an otherwise harmless repeated setup command can redisclose a long-lived wallet secret. Standard output is commonly captured by: - Agent execution transcripts and model context. - Shell history wrapp ...[truncated 2578 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the `privateKey` field from all normal and JSON output, including the existing-wallet branch. 2. Return only the public wallet address and a confirmation that key generation or loading succeeded. 3. Store the key in an operating-system credential manager, hardware-backed keystore, or encrypted wallet file where practical. 4. If a plaintext file must be supported: - Create `~/.pulse` with mode `0700`. - Create the key file atomically with mode `0600`. - Reject symbolic links. - Verify file ownership. - Correct unsafe permissions on existing files before reading or writing. 5. Avoid overwriting the secret file in place. Write to a securely created temporary file in the same protected directory, flush it, set its mode, and atomically rename it. 6. Provide a dedicated, interactive backup or export operation only if key export is essential. Require explicit confirmation and write directly to a protected destination rather than stdout. 7. Update `SKILL.md` and `README.md` so JSON mode is not described as a mechanism for obtaining or recording a private key. 8. Add automated tests asserting that no command output contains the configured private key. 9. Advise existing users to rotate wallets if JSON output may have been retained in transcripts, logs, or automation artifacts. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (32)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description centers on an on-chain commerce marketplace for AI services on MegaETH, including browse/buy/sell and escrow flows. The supplied code does not implement any marketplace actions. Instead, it provides CLI subcommands for registering an agent in an identity registry, initializing it in Pulse, fetching agent details, and updating the operator address. This is a materially different primary purpose: agent identity and administration rather than commerce. Therefore the description does not accurately represent the actual behavior.

Tp4

High
Category
MCP Tool Poisoning
Confidence
88% confidence
Finding
The declared description presents a full agent-to-agent commerce marketplace experience on MegaETH, including browsing, buying, and selling AI services with escrow. The supplied code chunk instead implements only a provider-side serving command: it starts a runtime that listens for incoming jobs, optionally loads a handler from disk, auto-accepts jobs, and can auto-deliver placeholder content. This is related at a high level to fulfilling marketplace jobs, but it does not implement the broader marketplace functions claimed in the description, especially browsing, purchasing, or escrow management. The code also exposes a concrete CLI trigger (`serve start`) despite no triggers being declared. Therefore the description does not accurately represent this specific code chunk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared description is about an on-chain AI services marketplace with escrow and agent-to-agent commerce. The supplied code chunk does not implement marketplace actions at all. Instead, it provides CLI wallet utilities: querying ETH and ERC-20 balances, generating a private key, deriving an address, and saving the key to ~/.pulse/config.json. These are materially different capabilities from the declared purpose. While wallet functionality could support a commerce tool, this chunk’s primary behavior is wallet management, not browsing, buying, selling, or escrow-based marketplace interaction.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The declared purpose describes a commerce/marketplace skill on MegaETH with browsing, buying, selling, and escrow functionality. The supplied code does not implement any marketplace actions, escrow logic, service discovery, or transaction workflows related to agent-to-agent commerce. Instead, it handles credential/config management and client initialization for Pulse mainnet, including filesystem access to ~/.pulse/config.json and use of a private key. These are materially different behaviors and resources from the stated purpose, so this is a clear description-to-behavior mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The supplied code does not implement marketplace, escrow, MegaETH, agent-to-agent commerce, browsing, buying, or selling functionality. Instead, it is a generic console output helper for formatting and printing results in JSON or human-readable terminal form. While such a module could be a supporting utility within a larger commerce skill, this chunk by itself is unrelated to the declared primary purpose and demonstrates materially different behavior.

Missing User Warnings

High
Confidence
98% confidence
Finding
The wallet generation flow both persists the private key and may echo it back to the caller without a strong safety warning. In this skill's context—an on-chain commerce CLI that can spend assets and manage agents—private key disclosure directly enables theft of funds, agent hijacking, and fraudulent marketplace actions.

Missing User Warnings

High
Confidence
98% confidence
Finding
When an existing key is present, JSON mode includes the raw private key in command output, which can be captured by shell history, logs, CI systems, terminal scrollback, or other automation consuming stdout. Because this skill manages blockchain wallets for marketplace activity, disclosure of the private key enables full account takeover and theft of funds or abuse of delegated operator permissions.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The README instructs users to generate a wallet and notes that the private key is saved automatically to ~/.pulse/config.json, but it does not warn that this file contains highly sensitive credentials or advise on securing it. In a blockchain commerce skill where the wallet can authorize purchases and receive or release funds, this omission increases the risk of accidental key exposure, insecure backups, or unsafe filesystem permissions.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The README presents buy, create-job, settle, and related payment-affecting commands as simple quick-start steps without warning that these trigger real blockchain actions that may be irreversible and can spend or release user funds. In the context of an on-chain marketplace with escrow, users may execute commands in production without realizing the financial consequences.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill declares no explicit tool scope even though it clearly requires network access and local environment/file interactions, including wallet generation and persistence. In an agent framework, missing permission boundaries can cause the skill to be granted broader capabilities than users expect, increasing the chance of unintended external calls or sensitive local-state access.

Session Persistence

Medium
Category
Rogue Agent
Content
When a user asks you to do something you can't do directly, search the Pulse marketplace for a specialized agent:

1. **Search**: `pulse browse "image generation" --json` to find relevant offerings
2. **Create Job**: `pulse job create --offering <id> --agent-id <your-agent-id> --json`
3. **Wait**: `pulse job status <jobId> --wait --json` to poll until completion
4. **Return results** to the user
Confidence
83% confidence
Finding
The workflow tells the agent to create a third-party job, wait for completion, and return results, which introduces cross-session state and delegation of user requests to an external service. That is risky because sensitive prompts or data may be sent to another agent and later returned without strong consent, provenance checks, or handling rules for pending jobs across sessions.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill instructs the agent to generate a wallet and save the keypair to `~/.pulse/config.json` without any explicit warning about secret handling, encryption, file permissions, backup risk, or multi-tenant exposure. Because this wallet is tied to on-chain identity and funds, insecure storage or casual disclosure can lead to theft, impersonation, or unauthorized marketplace actions.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
`saveConfig` writes the wallet private key to `~/.pulse/config.json` in plaintext without setting restrictive file permissions or using a secure secret store. On multi-user systems, backup agents, developer tooling, or malware can read this file and obtain full control of the associated wallet.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
The wallet generation command stores a raw private key locally and, in JSON mode, includes the private key in command output. Any logging pipeline, shell history wrapper, CI runner, or downstream tool consuming JSON output could capture and persist the secret, leading to full wallet compromise and unauthorized on-chain transactions.

Context-Inappropriate Capability

Medium
Confidence
93% confidence
Finding
The `serve start --handler` path resolves a user-supplied file path and dynamically imports it, then executes exported handler logic inside the provider runtime. This is effectively arbitrary code execution in the context of the user's wallet-bearing process, so a malicious or trojanized handler can steal private keys, sign transactions, exfiltrate job data, or alter marketplace behavior.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
jobCommand
  .command('create')
  .description('Create a new job (auto-approves USDm, deploys WARREN terms)')
  .requiredOption('--offering <id>', 'Offering ID')
  .requiredOption('--agent-id <id>', 'Your (buyer) agent ID')
  .option('--requirements <json>', 'JSON requirements to attach')
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.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
jobCommand
  .command('create')
  .description('Create a new job (auto-approves USDm, deploys WARREN terms)')
  .requiredOption('--offering <id>', 'Offering ID')
  .requiredOption('--agent-id <id>', 'Your (buyer) agent ID')
  .option('--requirements <json>', 'JSON requirements to attach')
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
91% confidence
Finding
The settle command triggers release of escrowed payment immediately with no interactive confirmation, dry-run summary, or explicit user acknowledgment. In a commerce CLI handling real on-chain funds, a mistyped job ID, automation mistake, or compromised agent workflow could irreversibly release payment before the operator notices.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The cancel command performs an irreversible job cancellation transaction without warning or confirmation. Because this is an on-chain marketplace command affecting escrow and job lifecycle state, accidental invocation or parameter mix-ups can cause financial loss, workflow disruption, and potentially forfeited business opportunities.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The command generates a new wallet private key and persists it to ~/.pulse/config.json without any visible warning, confirmation, or indication that this file now contains highly sensitive signing material. In an agent-to-agent commerce context, that key likely controls funds and on-chain permissions, so silent local storage increases the chance of accidental exposure through backups, weak file permissions, multi-user systems, or operator misunderstanding.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The code persists a wallet private key to ~/.pulse/config.json in plaintext using fs.writeFileSync, with no file-permission hardening, encryption, or user-facing warning in this file. In the context of an on-chain commerce skill, compromise of this file can directly lead to wallet takeover and irreversible asset loss, making the practical impact higher than a generic config-secret issue.

Session Persistence

Medium
Category
Rogue Agent
Content
}

/**
 * Create a Pulse client from PULSE_PRIVATE_KEY env var.
 * All contract addresses and indexer URL are embedded in the SDK.
 */
export function getClient(): PresetPulseClient {
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@pulseai/sdk": "^0.1.0",
    "commander": "^12.0.0",
    "viem": "^2.21.0",
    "chalk": "^5.3.0"
Confidence
96% confidence
Finding
The manifest uses a caret range for @pulseai/sdk, which permits automatic installation of newer minor and patch releases. For an agent skill that interacts with an on-chain marketplace and escrow flows, dependency drift can introduce unexpected behavior or a compromised upstream release into the runtime without any code change in this repository.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "dependencies": {
    "@pulseai/sdk": "^0.1.0",
    "commander": "^12.0.0",
    "viem": "^2.21.0",
    "chalk": "^5.3.0"
  },
Confidence
95% confidence
Finding
The commander dependency is not pinned and allows semver-compatible updates to be pulled at install time. Even for a CLI library, supply-chain compromise or a breaking behavioral change could affect command parsing and execution paths used by the skill.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "@pulseai/sdk": "^0.1.0",
    "commander": "^12.0.0",
    "viem": "^2.21.0",
    "chalk": "^5.3.0"
  },
  "devDependencies": {
Confidence
97% confidence
Finding
The viem dependency is unpinned, allowing newer releases to be installed automatically. Because this skill is for agent-to-agent commerce on MegaETH with escrow, an unexpected upstream change in blockchain transaction handling or signing logic has elevated risk compared with a purely local utility.

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
dist/pulse.js:124

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
dist/pulse.js:102

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
src/commands/wallet.ts:79