Back to skill

Security audit

zoho-support-claw

Security checks for vulnerabilities and agentic risk

Overview

This Zoho support skill is coherent, but it needs review because it sends and stores broad support ticket content with weak scoping and credential-destination controls.

Review this before installing in any real support environment. Use only least-privilege Zoho tokens, restrict ZOHO_DOMAIN to known Zoho Desk domains, confirm OpenAI processing is approved for your ticket data, redact sensitive fields before embedding or draft generation, and protect or regularly delete data/embeddings.json. Do not rely on drafts without human review.

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

T09 · Insecure Skill Coding Practices

Error
Location
lib/zohoClient.js:4
Finding
Zoho OAuth Token Can Be Forwarded to an Arbitrary Configured Host<![CDATA[ ## Vulnerability Details **File Location**: `lib/zohoClient.js:4-10` **Vulnerability Type**: Unvalidated credential destination **Risk Level**: High ### Vulnerable Code ```js const ZOHO_DOMAIN = process.env.ZOHO_DOMAIN || 'desk.zoho.com'; const ZOHO_TOKEN = process.env.ZOHO_TOKEN; if(!ZOHO_TOKEN) log.warn('ZOHO_TOKEN not set — Zoho API calls will fail'); const client = axios.create({ baseURL: `https://${ZOHO_DOMAIN}/api/v1`, headers: { Authorization: `Zoho-oauthtoken ${ZOHO_TOKEN}` } }); ``` ### Technical Analysis The application obtains `ZOHO_DOMAIN` directly from an environment variable and interpolates it into the Axios base URL without validating it against a list of legitimate Zoho endpoints. The Zoho OAuth token is then unconditionally attached to requests made through this client. An attacker who can influence the process environment or deployment configuration can set `ZOHO_DOMAIN` to an attacker-controlled HTTPS host. When either application command makes a Zoho request, the request will include the valid OAuth token in its `Authorization` header. This issue does not provide an unauthenticated remote attacker with direct control over the domain. Exploitation requires the ability to alter application environment variables or configuration, such as through a compromised deployment pipeline, unsafe container configuration, or administrative misconfiguration. ### Attack Path 1. The attacker gains the ability to modify the application's environment configuration. 2. The attacker sets `ZOHO_DOMAIN` to a host under their control, such as `collector.example`. 3. An operator executes `npm run ingest` or `npm run analyse`. 4. The Axios client sends a request to `https://collector.example/api/v1/tickets`. 5. The request includes `Authorization: Zoho-oauthtoken <token>`. 6. The attacker captures the token and uses it against the real Zoho Desk API, subject to the token's scopes and validity. ### Impact Assessment Successful exploitation d ...[truncated 375 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Validate `ZOHO_DOMAIN` against an explicit allowlist of supported Zoho Desk domains. - Reject values containing schemes, paths, user-information components, query strings, fragments, or unexpected ports. - Prefer selecting from fixed regional Zoho endpoints rather than accepting an arbitrary hostname. - Disable or strictly validate redirects for authenticated API requests so credentials cannot be forwarded to another origin. - Use a minimally scoped OAuth token and rotate it immediately if unauthorized forwarding is suspected. - Fail closed during startup when the domain is invalid. Example hardening: ```js const ALLOWED_ZOHO_DOMAINS = new Set([ 'desk.zoho.com', 'desk.zoho.eu', 'desk.zoho.in', 'desk.zoho.com.au' ]); const ZOHO_DOMAIN = process.env.ZOHO_DOMAIN || 'desk.zoho.com'; if (!ALLOWED_ZOHO_DOMAINS.has(ZOHO_DOMAIN)) { throw new Error('Unsupported ZOHO_DOMAIN'); } const client = axios.create({ baseURL: `https://${ZOHO_DOMAIN}/api/v1`, maxRedirects: 0, headers: { Authorization: `Zoho-oauthtoken ${ZOHO_TOKEN}` } }); ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
lib/vectorStore.js:6
Finding
Sensitive Support Ticket Data Is Persisted in Plaintext<![CDATA[ ## Vulnerability Details **File Location**: `lib/vectorStore.js:6-18` **Related Data Construction**: `index.js:14-22` **Vulnerability Type**: Insecure storage of sensitive information **Risk Level**: Medium ### Vulnerable Code Data placed into the local store: ```js const docs = tickets.map(t=>({ id: t.id, subject: t.subject || '', text: (t.subject||'') + '\n' + (t.description||'') + '\n' + (t.resolution||''), meta: { requester: t.contact, closed_at: t.modifiedTime } })); const embedResp = await embeddings.createEmbeddings(docs.map(d=>d.text)); const items = docs.map((d,i)=>({ id:d.id, text:d.text, vector: embedResp[i], meta:d.meta })); store.saveVectors(items); ``` Plaintext persistence: ```js const DATA_FILE = path.join(__dirname,'..','data','embeddings.json'); function ensureFile(){ const dir = path.dirname(DATA_FILE); if(!fs.existsSync(dir)) fs.mkdirSync(dir,{ recursive:true }); if(!fs.existsSync(DATA_FILE)) fs.writeFileSync(DATA_FILE, JSON.stringify({items:[]},null,2)); } function saveVectors(items){ ensureFile(); const store = JSON.parse(fs.readFileSync(DATA_FILE,'utf8')); store.items = store.items.concat(items); fs.writeFileSync(DATA_FILE, JSON.stringify(store,null,2)); log.info({count: items.length}, 'Saved vectors'); } ``` ### Technical Analysis The ingestion process stores complete ticket subjects, descriptions, resolutions, requester metadata, and embeddings in `data/embeddings.json`. The file is ordinary plaintext JSON and is created using default process permissions. No explicit restrictive mode, encryption, data minimization, retention limit, or deduplication mechanism is applied. Repeated ingestion appends records through `store.items.concat(items)`, which can retain duplicate and obsolete customer data indefinitely. Although exploitation requires filesystem, backup, artifact, or repository access, the file creates an additional sensitive-data repository outside Zoho's access controls. ### Attack Pa ...[truncated 879 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Store only the minimum fields required for similarity search; remove requester details and other unnecessary identifiers. - Redact credentials, payment data, personal information, and other sensitive patterns before embedding or persistence. - Encrypt the store at rest using a managed key appropriate to the deployment environment. - Create the directory and file with restrictive permissions, such as directory mode `0700` and file mode `0600`. - Add explicit retention, deletion, and deduplication policies keyed by ticket ID. - Ensure `data/` is excluded from source control, build artifacts, and public backups. - Document that embeddings and source ticket text are sensitive assets. - Apply operating-system access controls so only the service identity can read the file. - Consider storing only embeddings and a protected ticket reference rather than complete ticket text. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
lib/replyGenerator.js:8
Finding
Untrusted Ticket Content Enables Indirect Prompt Injection<![CDATA[ ## Vulnerability Details **File Location**: `lib/replyGenerator.js:8-16` **Related Input Flow**: `index.js:37-41` **Vulnerability Type**: Indirect prompt injection in LLM draft generation **Risk Level**: Medium ### Vulnerable Code ```js async function generateDraftReply(ticket, context){ try{ const system = `You are a helpful support agent. Use the ticket details and the context (similar closed tickets with resolutions) to draft a short professional reply (2-4 sentences) addressing the requester and proposing next steps.`; const user = `Ticket subject: ${ticket.subject}\nTicket description: ${ticket.description}\nContext: ${context}\n\nPlease provide a concise draft reply and a one-line summary of the recommended action.`; const resp = await client.chat.completions.create({ model: MODEL, messages: [ { role: 'system', content: system }, { role: 'user', content: user } ], max_tokens: 400 }); ``` The historical context is assembled from stored ticket text: ```js const queryText = (t.subject||'') + '\n' + (t.description||''); const qVec = await embeddings.createEmbeddings([queryText]); const nearest = store.findNearest(qVec[0], 5); const context = nearest.map(n=>n.text).join('\n---\n'); const draft = await replyGen.generateDraftReply(t, context); ``` ### Technical Analysis Current ticket fields and historical ticket text are untrusted or customer-controlled content. They are interpolated directly into a user message that also contains operational instructions. The prompt does not clearly delimit the data, identify it as untrusted, or instruct the model to disregard commands embedded inside ticket content. A malicious requester can include text such as instructions to ignore the support task, include attacker-selected links, disclose context, or produce misleading actions. The attack can occur directly through an open ticket or indirectly after a malicious ticket is ingested and retrieved as similar historical ...[truncated 1582 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Treat all current and historical ticket text as untrusted data. - Place ticket fields inside explicit structured delimiters and state in the system message that instructions inside those delimiters must never be followed. - Separate trusted task instructions from untrusted evidence as strongly as the model API permits. - Supply only the minimum relevant historical excerpts, excluding requester information and unnecessary ticket content. - Sanitize or flag instruction-like content, URLs, credential requests, and attempts to reveal context. - Validate generated drafts against policy before presenting them to staff. - Preserve mandatory human approval and clearly label output as untrusted AI-generated content. - Do not add automatic reply submission without further controls, authorization checks, and output filtering. - Where practical, request structured output and reject responses that do not conform to the expected schema. ]]>
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
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (20)

Missing User Warnings

High
Confidence
97% confidence
Finding
The README says the skill analyzes tickets and generates drafts using OpenAI, but does not clearly disclose that ticket contents may be transmitted to an external AI provider. Because support tickets often include sensitive customer and operational data, this omission creates a meaningful risk of unauthorized third-party disclosure, policy violations, and regulatory noncompliance.

Credential Access

High
Category
Privilege Escalation
Content
Configuration (in .env):
- ZOHO_DOMAIN (e.g. desk.zoho.com)
- ZOHO_TOKEN (OAuth access token)
- OPENAI_API_KEY
- OPENAI_MODEL (optional, default gpt-4o-mini or fallback)
- EMBEDDINGS_MODEL (optional, default text-embedding-3-small)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
Usage:
- Install via clawhub/publish or copy to workspace skills.
- Configure .env with ZOHO_TOKEN and OPENAI_API_KEY

Commands:
- ingest: Run historical ingest (npm run ingest)
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
Usage:
- Install via clawhub/publish or copy to workspace skills.
- Configure .env with ZOHO_TOKEN and OPENAI_API_KEY

Commands:
- ingest: Run historical ingest (npm run ingest)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The README says the skill analyzes tickets and generates drafts using OpenAI, but does not clearly disclose that ticket contents may be transmitted to an external AI provider. Because support tickets often include sensitive customer and operational data, this omission creates a meaningful risk of unauthorized third-party disclosure, policy violations, and regulatory noncompliance.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill description states that it ingests historical support tickets, stores local embeddings, and uses an OpenAI API key, but it does not warn users that ticket contents may be transmitted to external AI services and retained locally as embeddings. Support tickets often contain sensitive customer, account, or operational data, so failing to disclose these data flows creates a real privacy and compliance risk.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The code ingests full closed-ticket subject, description, and resolution text, then stores embeddings alongside requester metadata for later retrieval. Support tickets commonly contain personal, account, billing, or incident details, so bulk retention and vectorization of historical content materially increases privacy, data-governance, and cross-ticket data leakage risk.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
At this line, full ticket text is sent to an embeddings service, which is an external processing step for potentially sensitive support data. Without prior redaction, data-processing controls, or explicit disclosure/consent, this can expose customer information to third-party systems and create compliance and confidentiality risks.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The skill retrieves historical ticket text and uses it as context to generate replies for unrelated open tickets, which can leak sensitive information from prior cases into new responses. Because there is no filtering, authorization boundary, or redaction step on retrieved context, the RAG workflow can expose one customer's data to another or cause the model to reproduce confidential internal resolution details.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
Open ticket contents are embedded and then combined with retrieved historical text for draft generation, meaning current and prior sensitive support data may be transmitted to external model services. This raises both third-party disclosure risk and prompt-context leakage risk, especially because no sanitization, customer-boundary checks, or warning mechanisms are present.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
This code sends the provided `texts` to an external OpenAI embeddings endpoint, which may transmit user or system data off-host. While errors are logged, there is no confirmation prompt, user-facing notice, or explanatory comment/docstring in this file disclosing that text content is sent to a remote service.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The function sends raw ticket subject, description, and related context to the OpenAI API, which is an external third-party service. If tickets contain personal data, credentials, internal business information, or regulated content, this creates a privacy and data-governance risk when users are not clearly informed and the data is not minimized or sanitized before transmission.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This code persists supplied items, including `id`, `text`, and `meta`, into `data/embeddings.json` via `fs.writeFileSync`, which can retain user-derived content on disk. While there is an internal log message after saving, there is no user-facing prompt, warning comment, or explanatory docstring disclosing that data will be stored locally.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This code sends support ticket data through HTTP requests to the Zoho API, which can involve user or system data such as ticket subjects, descriptions, and requester/contact fields. While there is error logging and a missing-token warning, there is no comment, docstring, or user-facing disclosure here explaining that the skill retrieves and transmits potentially sensitive ticket content.

Missing User Warnings

Low
Confidence
80% confidence
Finding
The file reads an OAuth token from the environment via ZOHO_TOKEN, which is a sensitive credential access pattern covered by the warning requirement for code files. Although the code warns when the token is missing, it does not explain to users or maintainers that the skill depends on a credential and will authenticate to an external service with it.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"author": "",
  "license": "MIT",
  "dependencies": {
    "axios": "^1.4.0",
    "dotenv": "^16.0.0",
    "openai": "^4.0.0",
    "pino": "^8.0.0"
Confidence
97% confidence
Finding
The dependency version for axios is specified with a caret range, which allows npm to install newer minor and patch releases rather than a single vetted version. This weakens build reproducibility and can unintentionally introduce vulnerable or malicious upstream releases through the supply chain.

Unverifiable Dependency: axios has 16 known advisory(ies) (CVE-2026-44494 (axios Vulnerable to Full Man-in-the-Middle via Prototype Pollution Gadget in `co); CVE-2026-44495 (axios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollut); CVE-2025-62718 (Axios has a NO_PROXY Hostname Normalization Bypass that Leads to SSRF) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
94% confidence
Finding
The manifest includes axios without pinning to an exact version, while advisories exist for some axios releases. Because the installed version cannot be verified from this file alone, there is a credible risk that deployments may resolve to an affected version, especially across environments or over time.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "MIT",
  "dependencies": {
    "axios": "^1.4.0",
    "dotenv": "^16.0.0",
    "openai": "^4.0.0",
    "pino": "^8.0.0"
  }
Confidence
97% confidence
Finding
The dotenv dependency is not pinned to an exact version, so future installs may resolve to different package contents over time. That increases supply-chain risk and makes it harder to guarantee that the reviewed build matches the deployed build.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "axios": "^1.4.0",
    "dotenv": "^16.0.0",
    "openai": "^4.0.0",
    "pino": "^8.0.0"
  }
}
Confidence
97% confidence
Finding
The openai package uses a caret version range, allowing different releases to be installed without source changes. For a skill that processes support tickets and may handle sensitive data, non-reproducible dependency resolution increases the risk of unexpected behavior or supply-chain compromise.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"axios": "^1.4.0",
    "dotenv": "^16.0.0",
    "openai": "^4.0.0",
    "pino": "^8.0.0"
  }
}
Confidence
97% confidence
Finding
The pino dependency is declared with a non-exact semver range, which permits unreviewed upstream updates during installation. This creates avoidable supply-chain exposure and reduces the integrity and reproducibility of the application environment.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
lib/zohoClient.js:3