Back to skill

Security audit

Kash - Agentic Payment Provider

Security checks for vulnerabilities and agentic risk

Overview

This payment skill is mostly coherent and disclosed, but it has real safeguard gaps that could expose the API key or bypass local spending limits if misconfigured.

Review before installing. Use only a low-privilege Kash agent key, set server-side budgets in the Kash dashboard, keep KASH_API_URL unset unless doing local development, and set numeric budget and confirmation values carefully. Treat local budget and confirmation controls as helpful but not sufficient until the parsing and HTTPS validation issues are fixed.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
tools.ts:19
Finding
Malformed numeric configuration silently disables local spending safeguards<![CDATA[ ## Vulnerability Details **File Location**: `tools.ts:19-23`, `tools.ts:91-106` **Vulnerability Type**: Fail-open numeric configuration validation **Risk Level**: High ### Vulnerable Code ```ts const KASH_BUDGET = process.env.KASH_BUDGET ? parseFloat(process.env.KASH_BUDGET) : null const KASH_API_URL = process.env.KASH_API_URL || 'https://api.kash.dev' const SPEND_CONFIRMATION_THRESHOLD = parseFloat( process.env.KASH_SPEND_CONFIRMATION_THRESHOLD || '5.00' ) ``` ```ts if (params.amount <= 0) { return 'ERROR: amount must be greater than 0' } // ── Local KASH_BUDGET cap ──────────────────────────────────────────────────── if (KASH_BUDGET !== null && sessionSpent + params.amount > KASH_BUDGET) { return ( `LOCAL_BUDGET_EXCEEDED: Spending $${params.amount} would exceed local KASH_BUDGET of ` + `$${KASH_BUDGET} (session spent: $${sessionSpent.toFixed(4)}). ` + `Tell the user their local budget cap is reached. ` + `They can raise KASH_BUDGET in .env or top up at kash.dev/dashboard/wallets.` ) } // ── Confirmation gate ──────────────────────────────────────────────────────── if (params.amount > SPEND_CONFIRMATION_THRESHOLD && !params.confirmed) { ``` ### Technical Analysis The skill parses `KASH_BUDGET` and `KASH_SPEND_CONFIRMATION_THRESHOLD` with `parseFloat()` but does not verify that the resulting values are finite, nonnegative numbers. A malformed nonempty value such as `x` produces JavaScript's `NaN`. Ordered comparisons involving `NaN` evaluate to `false`. Therefore: - If `KASH_BUDGET=x`, the expression `sessionSpent + params.amount > KASH_BUDGET` is false, silently disabling the advertised local session cap. - If `KASH_SPEND_CONFIRMATION_THRESHOLD=x`, the expression `params.amount > SPEND_CONFIRMATION_THRESHOLD` is false, silently disabling the confirmation requirement for large transactions. - Startup succeeds rather than rejecting the unsafe configuration. The runtime `amount` check also ...[truncated 1315 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse configuration strictly during startup and fail closed on invalid values. - Use `Number(value)` rather than permissive partial parsing with `parseFloat()`. - Require both values to satisfy `Number.isFinite(value)` and `value >= 0`. - Apply equivalent validation to every `params.amount` received by `kash_spend`. - Reject malformed, infinite, negative, or otherwise unsupported numeric values before performing budget or confirmation comparisons. - Add automated tests for empty strings, alphabetic input, partially numeric strings, `NaN`, positive and negative infinity, negative values, zero, and valid decimal values. Example hardening pattern: ```ts function parseNonNegativeNumber(name: string, raw: string): number { const value = Number(raw) if (!Number.isFinite(value) || value < 0) { throw new Error(`[kash-skill] ${name} must be a finite, nonnegative number`) } return value } const KASH_BUDGET = process.env.KASH_BUDGET === undefined ? null : parseNonNegativeNumber('KASH_BUDGET', process.env.KASH_BUDGET) const SPEND_CONFIRMATION_THRESHOLD = parseNonNegativeNumber( 'KASH_SPEND_CONFIRMATION_THRESHOLD', process.env.KASH_SPEND_CONFIRMATION_THRESHOLD ?? '5.00' ) ``` At the tool boundary, enforce: ```ts if (!Number.isFinite(params.amount) || params.amount <= 0) { return 'ERROR: amount must be a finite number greater than 0' } ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
tools.ts:47
Finding
API endpoint allowlist permits plaintext transmission of the Kash credential<![CDATA[ ## Vulnerability Details **File Location**: `tools.ts:47-69`, with credential-bearing requests at `tools.ts:113-118` and `tools.ts:163-165` **Vulnerability Type**: Missing transport-protocol validation **Risk Level**: High ### Vulnerable Code ```ts const TRUSTED_DOMAINS = ['api.kash.dev', 'localhost', '127.0.0.1'] if (KASH_API_URL !== 'https://api.kash.dev') { try { const url = new URL(KASH_API_URL) const isTrusted = TRUSTED_DOMAINS.some( d => url.hostname === d || url.hostname.endsWith(`.${d}`) ) if (!isTrusted) { throw new Error( `[kash-skill] KASH_API_URL points to untrusted domain: "${url.hostname}".\n` + 'Only api.kash.dev and localhost are allowed.\n' + 'Remove KASH_API_URL from .env or use a trusted endpoint.' ) } } catch (e: any) { if (e.message.startsWith('[kash-skill]')) throw e throw new Error(`[kash-skill] KASH_API_URL is not a valid URL: ${KASH_API_URL}`) } } ``` The accepted URL is subsequently used for credential-bearing requests: ```ts const res = await fetch(`${KASH_API_URL}/api/agents/${KASH_AGENT_ID}/spend`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-kash-key': KASH_KEY!, }, ``` ```ts const res = await fetch(`${KASH_API_URL}/api/agents/${KASH_AGENT_ID}/balance`, { headers: { 'x-kash-key': KASH_KEY! }, }) ``` ### Technical Analysis The endpoint validation checks only `url.hostname`. It does not verify `url.protocol`, despite `SECURITY.md` stating that the credential is sent over HTTPS. As a result, a value such as `http://api.kash.dev` passes the hostname allowlist. Both balance and spending requests then include `KASH_KEY` in the `x-kash-key` header over an unencrypted HTTP connection. The allowlist also accepts subdomains through `hostname.endsWith()`, even though the documented production destination is specifically `api.kash.dev`. The confirmed credential-exposure pat ...[truncated 1280 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require `https:` for every non-loopback API endpoint. - If plaintext local development is necessary, allow `http:` only when the hostname is exactly `localhost`, `127.0.0.1`, or another explicitly supported loopback address. - Prefer exact hostname matches for production instead of allowing arbitrary subdomains. - Reject URLs containing embedded credentials, fragments, queries, or unexpected base paths. - Consider restricting production ports to the default HTTPS port. - Store the parsed and normalized URL and construct request URLs with the `URL` API rather than raw string concatenation. - Add startup tests proving that `http://api.kash.dev`, non-HTTP schemes, attacker-controlled hosts, misleading suffixes, and unauthorized subdomains are rejected. Example protocol enforcement: ```ts const url = new URL(KASH_API_URL) const isLoopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' const validProductionEndpoint = url.protocol === 'https:' && url.hostname === 'api.kash.dev' const validDevelopmentEndpoint = isLoopback && (url.protocol === 'http:' || url.protocol === 'https:') if (!validProductionEndpoint && !validDevelopmentEndpoint) { throw new Error( '[kash-skill] KASH_API_URL must use HTTPS with api.kash.dev ' + 'or an explicitly permitted loopback development endpoint' ) } ``` ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (3)

Credential Access

High
Category
Privilege Escalation
Content
throw new Error(
        `[kash-skill] KASH_API_URL points to untrusted domain: "${url.hostname}".\n` +
        'Only api.kash.dev and localhost are allowed.\n' +
        'Remove KASH_API_URL from .env or use a trusted endpoint.'
      )
    }
  } catch (e: any) {
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
throw new Error(
        `[kash-skill] KASH_API_URL points to untrusted domain: "${url.hostname}".\n` +
        'Only api.kash.dev and localhost are allowed.\n' +
        'Remove KASH_API_URL from .env or use a trusted endpoint.'
      )
    }
  } catch (e: any) {
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill uses sensitive environment variables and network-backed payment capabilities, but it does not declare a restrictive tool scope such as permissions or allowed-tools. That omission increases the attack surface because a host agent may expose broader capabilities than intended, making it easier for prompt injection or skill misuse to access secrets or trigger external payment actions outside a clearly bounded interface.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
tools.ts:19