Back to skill

Security audit

CRMy

Security checks for vulnerabilities and agentic risk

Overview

This CRM skill mostly does what it claims, but it handles API keys and sensitive CRM updates without enough transport and input-safety controls.

Install only if you trust the CRMy server and API key scope. Prefer HTTPS server URLs except local development, use a least-privilege CRMy API key, review agent-requested CRM writes before approval, and avoid passing untrusted record IDs into update tools until the plugin validates UUIDs.

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

Warning
Location
src/index.ts:104
Finding
Agent-Controlled Identifier Enables Authenticated API Path Traversal<![CDATA[ ## Vulnerability Details **File Location**: `src/index.ts:104`, `src/index.ts:164`, and `src/index.ts:253` **Vulnerability Type**: Improper validation and encoding of URL path components **Risk Level**: Medium The same vulnerable behavior is present in the shipped executable at `dist/index.js`. ### Vulnerable Code ```ts handler: async ({ id, ...rest }) => client.patch(`/contacts/${id as string}`, rest), ``` ```ts handler: async ({ id, ...rest }) => client.patch(`/contacts/${id as string}`, rest), ``` ```ts handler: async ({ id, ...rest }) => client.patch(`/opportunities/${id as string}`, rest), ``` The resulting endpoint is concatenated directly with the API base URL: ```ts async patch(endpoint: string, body: unknown): Promise<unknown> { const res = await fetch(`${this.base}${endpoint}`, { method: 'PATCH', headers: this.headers, body: JSON.stringify(body), }); return this.parse(res); } ``` ### Technical Analysis The `id` parameters are described as UUIDs, but their tool schemas only enforce `type: 'string'`. No runtime UUID validation or path-component encoding is applied before an identifier is interpolated into a URL. Consequently, an identifier can contain URL-significant sequences such as `../`, `/`, `?`, or `#`. URL parsing performed by `fetch` can normalize dot segments and alter the intended route. For example, an identifier such as `../admin/settings` changes: ```text /api/v1/contacts/../admin/settings ``` into an effective path equivalent to: ```text /api/v1/admin/settings ``` The request retains the plugin's CRMy bearer credential. Exploitation therefore depends on whether another same-origin route accepts `PATCH` and whether the configured API key is authorized for that route, but the plugin does not constrain the request to the intended contact or opportunity resource. ### Attack Path 1. An attacker influences an agent prompt, retrieved CRM content, or another untrusted input used as a record identifier. 2. ...[truncated 882 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce UUID syntax in every identifier schema, for example with an appropriate JSON Schema `format` or strict `pattern`. 2. Perform runtime validation before constructing the endpoint, because tool-schema validation should not be the only security boundary. 3. Reject identifiers containing path separators, dot segments, query delimiters, fragments, percent-encoded separators, or any characters outside the accepted UUID alphabet. 4. Encode path components as defense in depth: ```ts function requireUuid(value: unknown): string { if ( typeof value !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) ) { throw new Error('Invalid record UUID'); } return value; } handler: async ({ id, ...rest }) => { const uuid = requireUuid(id); return client.patch(`/contacts/${encodeURIComponent(uuid)}`, rest); }; ``` 5. Consider changing the client API to accept validated resource names and identifiers separately rather than accepting arbitrary endpoint strings. 6. Add regression tests using values such as `../admin`, `%2e%2e%2fadmin`, `id/child`, `id?x=y`, and `id#fragment`, and verify that all are rejected before a network request is made. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/client.ts:23
Finding
Bearer Credentials and CRM Data Can Be Transmitted over Remote Plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `src/client.ts:23-35` and `src/client.ts:41-76` **Vulnerability Type**: Cleartext transmission of sensitive information **Risk Level**: Medium The same behavior is present in the shipped executable at `dist/index.js`. The plugin configuration schema accepts an unrestricted server URL and documents an HTTP URL as its example/default. ### Vulnerable Code ```ts const serverUrl = pluginConfig?.serverUrl ?? process.env.CRMY_SERVER_URL ?? fileConfig.serverUrl ?? 'http://localhost:3000'; const apiKey = pluginConfig?.apiKey ?? process.env.CRMY_API_KEY ?? fileConfig.apiKey ?? ''; return { serverUrl: serverUrl.replace(/\/$/, ''), apiKey }; ``` ```ts constructor(cfg: CrmyClientConfig) { this.base = `${cfg.serverUrl}/api/v1`; this.headers = { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json', }; } ``` ```ts async post(endpoint: string, body: unknown): Promise<unknown> { const res = await fetch(`${this.base}${endpoint}`, { method: 'POST', headers: this.headers, body: JSON.stringify(body), }); return this.parse(res); } async patch(endpoint: string, body: unknown): Promise<unknown> { const res = await fetch(`${this.base}${endpoint}`, { method: 'PATCH', headers: this.headers, body: JSON.stringify(body), }); return this.parse(res); } ``` The corresponding plugin configuration permits any string: ```json "serverUrl": { "type": "string", "description": "CRMy server URL. Defaults to the value in ~/.crmy/config.json, then http://localhost:3000." } ``` ### Technical Analysis Using HTTP for a service bound exclusively to loopback may be acceptable, but `resolveConfig` does not distinguish loopback endpoints from remote hosts. It accepts arbitrary `http://` URLs from plugin configuration, environment variables, or `~/.crmy/config.json`. All requests include an `Authorization: Bearer` header. Mutation requests can also contain ...[truncated 1618 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `serverUrl` with `new URL()` during configuration resolution and reject malformed URLs. 2. Require the `https:` protocol for all non-loopback destinations. 3. Permit `http:` only for explicitly recognized loopback hosts such as `localhost`, `127.0.0.1`, and `[::1]`; do not rely on hostname suffix matching. 4. Reject unsupported schemes, embedded credentials, unexpected fragments, and ambiguous URLs. 5. Add equivalent restrictions to `openclaw.plugin.json`, while retaining runtime validation as the authoritative control. 6. Fail closed with a clear error rather than silently accepting insecure remote transport. 7. Rotate any API key that may previously have been used with a remote plaintext endpoint. 8. Apply least-privilege scopes and expiration to API keys so that exposure has limited impact. Example validation: ```ts function validateServerUrl(value: string): string { const url = new URL(value); const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'; if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) { throw new Error('CRMy serverUrl must use HTTPS except for loopback services'); } if (url.username || url.password || url.hash) { throw new Error('CRMy serverUrl contains unsupported URL components'); } return url.toString().replace(/\/$/, ''); } ``` ]]>
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
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (13)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description promises a specific CRM automation skill with concrete domain behaviors. However, the provided code chunk is merely a .d.ts type definition file for a generic tool/plugin API. It defines interfaces such as ToolDef and OpenClawApi and a default function signature, but does not implement any tool, CRM integration, data access, search, creation, logging workflow, or recommendation logic. This is a material mismatch because the actual supplied code does not substantiate the declared purpose at all.

Tp4

High
Category
MCP Tool Poisoning
Confidence
89% confidence
Finding
The overall domain and primary purpose align well with the declared CRM scope: the code works with contacts, accounts, opportunities/deals, activities, and pipeline summaries in CRMy. However, the description presents behavioral guarantees/policies ('Search before creating', 'Log every meaningful interaction', 'Always suggest next steps') that the code does not enforce or implement. Instead, it merely exposes independent tools, including search and activity logging as optional operations. Also, the code includes explicit update/advance-stage capabilities that are compatible with 'manages' but not specifically described. This is a description-to-behavior mismatch because the declared agent behavior is stronger and more prescriptive than what the code actually does.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill declares no explicit tool scope or permissions boundaries even though the analyzed content indicates capabilities associated with environment and network access. In an agent setting, missing scope constraints can let the model invoke broader-than-necessary capabilities, increasing the chance of data exfiltration, unintended external calls, or misuse of available runtime context.

Rp1

Medium
Category
MCP Rug Pull
Confidence
93% confidence
Finding
Referencing an MCP/server package via `npx @crmy/cli` without a pinned version creates a supply-chain risk because execution may resolve to the latest published package at runtime. If the package is compromised or a breaking version is released, the skill could run attacker-controlled code or unexpectedly change behavior.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This code accesses a sensitive credential via process.env.CRMY_API_KEY and file-based config, then uses it in outbound HTTP requests. The file contains no confirmation prompt, user-facing log/print, or explanatory comment/docstring warning that credentials will be used for network calls.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This skill sends contact, account, opportunity, notes, and activity data to an external CRM service through many network-enabled tools, but the code provides no in-file disclosure, consent gate, destination allowlist, or transport security enforcement. In an agent setting, that increases the risk of unintended disclosure of sensitive business or personal data, especially because serverUrl is configurable and may point to a non-local or even insecure endpoint.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This code registers multiple tools that create or update CRM data via POST and PATCH requests, including contacts, accounts, opportunities, lifecycle stages, and activity logs. While the tool descriptions state what each action does, there is no visible confirmation prompt, logger/print disclosure, or cautionary comment warning that these operations will persist changes to external customer records.

Missing User Warnings

Low
Confidence
76% confidence
Finding
This code file defines an optional `apiKey` field in configuration, which indicates the skill may access credentials or other sensitive authentication material. There is no adjacent comment, docstring, or user-facing disclosure explaining that sensitive credentials may be handled, which matches the missing-warning criterion for code files.

Context-Inappropriate Capability

Low
Confidence
79% confidence
Finding
The manifest describes a CRM agent that manages contacts, accounts, deals, pipeline, and activity logging. The code also accesses the local filesystem and process environment to load configuration and API secrets, which is an additional capability not described in that user-facing purpose statement.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dev": "tsup src/index.ts --format esm --watch"
  },
  "devDependencies": {
    "tsup": "^8.3.0",
    "typescript": "^5.6.3"
  },
  "openclaw": {
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unverifiable Dependency: tsup has 1 known advisory(ies) (CVE-2024-53384 (tsup DOM Clobbering vulnerability)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "devDependencies": {
    "tsup": "^8.3.0",
    "typescript": "^5.6.3"
  },
  "openclaw": {
    "extensions": [
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
dist/index.js:12

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
src/client.ts:25