Back to skill

Security audit

Skill Vexa

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches Vexa meeting automation, but its optional webhook path can turn external meeting events into agent commands and persistent memory updates with limited validation.

Review this before installing if you plan to use webhooks or automatic reports. The normal CLI flows are coherent for Vexa, but enabling the webhook can let an external event wake the agent, fetch transcripts, write reports, and alter persistent entity memory. Use only trusted webhook endpoints, avoid custom VEXA_BASE_URL values unless you control them, and manually review transcript-derived memory changes.

Vulnerability Patterns
  • 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
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T01 · Skill Instruction Hijacking

Error
Location
scripts/vexa-transform.mjs:11
Finding
Untrusted Webhook Payload Is Converted into Agent Instructions and an Executable Command<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vexa-transform.mjs:11-74` **Vulnerability Type**: Webhook-driven Agent instruction and command injection **Risk Level**: Critical ### Vulnerable Code ```js const p = ctx?.payload || {}; // Extract platform + native_meeting_id from common payload shapes const platform = (typeof p.platform === 'string' && p.platform) || (typeof p.meeting?.platform === 'string' && p.meeting.platform) || (typeof p.data?.platform === 'string' && p.data.platform) || ''; const nativeMeetingId = (typeof p.native_meeting_id === 'string' && p.native_meeting_id) || (typeof p.meeting?.native_meeting_id === 'string' && p.meeting.native_meeting_id) || (typeof p.data?.native_meeting_id === 'string' && p.data.native_meeting_id) || ''; if (!platform || !nativeMeetingId) { return null; // skip — no meeting identity } // Only process when meeting is finished (skip in-progress / heartbeat) const status = ( (typeof p.status === 'string' && p.status) || (typeof p.meeting?.status === 'string' && p.meeting.status) || (typeof p.data?.status === 'string' && p.data.status) || '' ).toLowerCase(); const event = ( (typeof p.event === 'string' && p.event) || (typeof p.event_type === 'string' && p.event_type) || '' ).toLowerCase(); const completionReason = p.data?.completion_reason ?? p.meeting?.data?.completion_reason; const isFinished = ['completed', 'finalized', 'done'].includes(status) || (event && (event.includes('complete') || event.includes('final'))) || Boolean(completionReason); const isInProgress = ['active', 'in_progress', 'running'].includes(status); if (isInProgress) { return null; // skip — meeting still running } if (!isFinished && (status || event)) { return null; // skip — has status/event but not finished } // Meeting finished — inject explicit report command const reportCmd = `node skills/vexa/scripts/vexa.mjs report --platform ${platform} --native_meeting_id ${nativeMeetingI ...[truncated 3292 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Verify webhook authenticity before invoking the transform, preferably with a provider-issued HMAC signature, timestamp, and replay protection. 2. Reject events that do not have an explicit, exact completion status. Do not treat missing `status` and `event` fields as completion. 3. Allow only known platforms: - `google_meet` - `teams` - `zoom` 4. Validate meeting IDs with platform-specific regular expressions before further processing. 5. Do not construct a textual shell command from webhook values. Invoke a fixed local handler with structured arguments or use `spawn`/`execFile` with `shell: false` and a fixed executable. 6. Do not place the raw payload in an Agent instruction. If diagnostic data is required, store a filtered representation separately and clearly label it as inert, untrusted data. 7. Use a deterministic report-generation worker for webhook events rather than asking a general-purpose Agent to interpret and execute the payload. 8. Apply length limits and reject control characters, newlines, shell metacharacters, and unexpected properties. 9. Add tests covering missing statuses, forged completion reasons, prompt-injection strings, newline injection, and shell metacharacters. ]]>

T02 · Agent Memory Poisoning

Error
Location
scripts/vexa-transform.mjs:57
Finding
Webhook-Triggered Transcript Processing Can Poison Persistent Agent Memory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vexa-transform.mjs:57-74` **Vulnerability Type**: Automatic persistence of untrusted meeting-derived content **Risk Level**: High ### Vulnerable Code ```js // Meeting finished — inject explicit report command const reportCmd = `node skills/vexa/scripts/vexa.mjs report --platform ${platform} --native_meeting_id ${nativeMeetingId}`; const message = `Vexa meeting finished webhook received. Extracted: platform=${platform}, native_meeting_id=${nativeMeetingId} Task: 1. Run: ${reportCmd} (creates basic meeting report in memory/meetings/) 2. Open the generated report and add a Summary section with 5-10 bullets 3. Update/create entity files under memory/entities/ (products, companies, people) referenced in the report 4. Reply with: report path + entities updated + any issues 5. When sending anything to meeting chat, use PLAIN TEXT only (no markdown) -- Google Meet chat does not render markdown. Raw payload (for reference): ${JSON.stringify(p, null, 2)}`; return { message }; ``` ### Technical Analysis The webhook message directs the Agent to retrieve a remote transcript, summarize it, and update persistent files under `memory/entities/`. Meeting speech, speaker names, meeting metadata, and the webhook payload are externally influenced content. No review gate, provenance requirement, schema constraint, or prompt-injection filtering is applied before this content is used to update long-term Agent memory. Consequently, adversarial transcript text can be interpreted as instructions rather than meeting data, while fabricated claims can be persisted as facts about products, companies, or people. This exceeds the minimum privilege required to generate the declared meeting report. A basic report can be generated under `memory/meetings/` without automatically changing global entity memory that may influence unrelated future sessions. ### Attack Path 1. An attacker joins or otherwise controls content in a ...[truncated 1087 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove automatic updates to `memory/entities/` from webhook-triggered workflows. 2. Require explicit user approval before promoting transcript-derived claims into persistent memory. 3. Generate proposed entity changes in a quarantine or staging file and present a diff for review. 4. Treat transcripts and webhook payloads as untrusted data, never as Agent instructions. 5. Use schema-constrained extraction with strict field and length limits. 6. Record provenance for every persisted claim, including meeting identity, timestamp, speaker attribution, and confidence. 7. Strip or flag instruction-like phrases before content reaches an Agent context. 8. Restrict the webhook worker's filesystem permissions to `memory/meetings/` unless a separate approved operation grants entity-memory access. 9. Add rollback and conflict-resolution mechanisms for generated memory updates. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/vexa.mjs:103
Finding
API Keys Can Be Transmitted to Arbitrary or Plaintext Custom Endpoints<![CDATA[ ## Vulnerability Details **File Location**: `scripts/vexa.mjs:103-123, 300-316, 538-561` **Vulnerability Type**: Insufficient destination validation for authenticated network requests **Risk Level**: High ### Vulnerable Code ```js function resolveBaseUrl() { // Explicit env var always wins if (process.env.VEXA_BASE_URL?.trim()) return process.env.VEXA_BASE_URL.trim().replace(/\/$/, ""); const config = loadEndpointsConfig(); const entry = getEndpointEntry(config, config?.active); if (entry?.url) return entry.url.replace(/\/$/, ""); return "https://api.cloud.vexa.ai"; } function resolveApiKey() { // Explicit env var always wins if (process.env.VEXA_API_KEY?.trim()) { // But check if active endpoint has its own key — prefer that const config = loadEndpointsConfig(); const entry = getEndpointEntry(config, config?.active); if (entry?.apiKey) return entry.apiKey; return process.env.VEXA_API_KEY.trim(); } // Try endpoint-specific key const config = loadEndpointsConfig(); const entry = getEndpointEntry(config, config?.active); if (entry?.apiKey) return entry.apiKey; return process.env.VEXA_API_KEY || null; } async function vexaFetch(path, { method = "GET", body } = {}) { if (!API_KEY) die("Missing VEXA_API_KEY. Source ~/.openclaw/secrets/vexa.env or run: node skills/vexa/scripts/onboard.mjs"); const url = `${BASE_URL}${path}`; const headers = { "X-API-Key": API_KEY, Accept: "application/json" }; if (body !== undefined) headers["Content-Type"] = "application/json"; const res = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); const text = await res.text(); let json; try { json = text ? JSON.parse(text) : null; } catch { json = { raw: text }; } if (!res.ok) { const msg = typeof json === "object" ? JSON.stringify(json, null, 2) : String(json); die(`Vexa API error ${res.status} ${res.statusText}\n${msg}`); } ...[truncated 3120 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse every configured endpoint with `new URL()` and reject malformed URLs. 2. Require HTTPS for all remote endpoints. 3. Permit HTTP only when the resolved hostname is an actual loopback address such as `127.0.0.1` or `::1`. 4. Maintain a trusted-host allowlist for production use, or require explicit interactive confirmation before sending credentials to a newly configured origin. 5. Bind endpoint-specific API keys to an exact normalized origin and refuse to reuse the general production key on a different origin. 6. Ignore or tightly control `VEXA_BASE_URL` in privileged or automated webhook contexts. 7. Display the destination origin before the first authenticated request to a custom endpoint. 8. Apply identical validation in `vexa.mjs`, `onboard.mjs`, `ingest.mjs`, and `audit.mjs`. 9. Consider storing endpoint keys outside `vexa-endpoints.json` in a dedicated secret store, even though the current file is created with mode `0600`. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/onboard.mjs:155
Finding
Webhook Validation Reads Global Agent Session Data and Reuses a Privileged Hook Token<![CDATA[ ## Vulnerability Details **File Location**: `scripts/onboard.mjs:155-203, 274-313` **Vulnerability Type**: Excessive access to OpenClaw session and authentication data **Risk Level**: Medium ### Vulnerable Code ```js function checkWebhookReceived(withinMinutes = 10) { const sessionsPaths = [ path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions', 'sessions.json'), process.env.OPENCLAW_HOME ? path.join(process.env.OPENCLAW_HOME, 'agents', 'main', 'sessions', 'sessions.json') : '' ].filter(Boolean); const cutoff = Date.now() - withinMinutes * 60 * 1000; for (const p of sessionsPaths) { try { const raw = fs.readFileSync(p, 'utf8'); const data = JSON.parse(raw); const keys = Object.keys(data || {}); const vexaHookKeys = keys.filter((k) => k.startsWith('agent:main:hook:vexa:meeting:')); const recent = vexaHookKeys.filter((k) => { const updatedAt = data[k]?.updatedAt; return typeof updatedAt === 'number' && updatedAt >= cutoff; }); return { webhook_received: recent.length > 0, recent_vexa_sessions: recent, within_minutes: withinMinutes }; } catch (e) { if (e?.code === 'ENOENT') continue; return { webhook_received: false, error: String(e?.message || e) }; } } return { webhook_received: false, error: 'sessions.json not found' }; } function checkWebhookConfig() { const configPaths = [ path.join(os.homedir(), '.openclaw', 'openclaw.json'), process.env.OPENCLAW_CONFIG || '', process.env.OPENCLAW_HOME ? path.join(process.env.OPENCLAW_HOME, 'openclaw.json') : '' ].filter(Boolean); for (const p of configPaths) { try { const raw = fs.readFileSync(p, 'utf8'); const cfg = JSON.parse(raw); const mappings = cfg?.hooks?.mappings || []; const hasVexa = Array.isArray(mappings) && mappings.some((m) => m?.id === 'vexa'); return { webhook_configured: hasVexa, config_path: p }; ...[truncated 3672 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace global `sessions.json` parsing with a dedicated webhook receipt marker or narrowly scoped status API. 2. Do not return internal Agent session identifiers in command output. 3. Use a dedicated, short-lived test credential restricted to the Vexa hook instead of the general `hooks.token`. 4. Separate mock webhook testing from standard onboarding and require explicit user initiation. 5. Run validation through a deterministic local test harness that calls the transform directly with a fixed payload where possible. 6. Restrict accepted configuration paths and verify ownership and permissions before reading them. 7. Avoid retaining hook credentials in general process state longer than required. 8. Document the additional access to OpenClaw configuration and session metadata and obtain user consent before validation. 9. Apply filesystem sandboxing so normal Vexa API operations cannot access Agent session or global configuration files. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
Findings (23)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
- `POST /bots` — request bot (`platform`, `native_meeting_id`, optional `language`, `bot_name`; Teams also needs `passcode`)
- `GET /bots/status` — list running bots
- `PUT /bots/{platform}/{native_meeting_id}/config` — update active bot config (e.g., language)
- `DELETE /bots/{platform}/{native_meeting_id}` — stop bot
- `GET /transcripts/{platform}/{native_meeting_id}` — transcript (during or after meeting)
- `POST /transcripts/{platform}/{native_meeting_id}/share` — temporary share URL
- `GET /meetings` — meeting history
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
- `POST /transcripts/{platform}/{native_meeting_id}/share` — temporary share URL
- `GET /meetings` — meeting history
- `PATCH /meetings/{platform}/{native_meeting_id}` — update metadata (`data.name`, `data.participants`, `data.languages`, `data.notes`)
- `DELETE /meetings/{platform}/{native_meeting_id}` — purge transcript + anonymize finalized meeting only
- `PUT /user/webhook` — set user webhook URL
- `GET /voice-agent-config` — get user's voice agent config (incl. custom `ultravox_system_prompt`)
- `PUT /voice-agent-config` — update voice agent config; set `ultravox_system_prompt` to a string to override, or `null` to reset to service default
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
- `POST /transcripts/{platform}/{native_meeting_id}/share` — temporary share URL
- `GET /meetings` — meeting history
- `PATCH /meetings/{platform}/{native_meeting_id}` — update metadata (`data.name`, `data.participants`, `data.languages`, `data.notes`)
- `DELETE /meetings/{platform}/{native_meeting_id}` — purge transcript + anonymize finalized meeting only
- `PUT /user/webhook` — set user webhook URL
- `GET /voice-agent-config` — get user's voice agent config (incl. custom `ultravox_system_prompt`)
- `PUT /voice-agent-config` — update voice agent config; set `ultravox_system_prompt` to a string to override, or `null` to reset to service default
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
## Safety

- Treat `DELETE /meetings/...` as destructive.
- Require explicit user confirmation for exact meeting identity.
- If delete returns `409`, the meeting is not finalized yet.
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill invokes shell commands, uses environment-based secrets, and talks to external network services, but it does not declare an explicit tool scope such as permissions or allowed-tools. That creates an overly broad execution surface where an agent may run commands or access secrets without a clear policy boundary, increasing the chance of unintended command execution or data exposure.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
**Webhook — proactive setup:** Whenever secrets exist and you're working with Vexa, run `node skills/vexa/scripts/onboard.mjs --check-webhook`. If `webhook_configured` is false, advise adding the vexa mapping (see `references/webhook-setup.md`). For onboarding-only details (webhook requirements, pipeline validation, mock webhook), load `references/onboarding-flow.md`.

**When secrets are missing:** Direct the user to get their key from https://vexa.ai/dashboard/api-keys and set it up themselves. They should either set `VEXA_API_KEY` in their environment or create `skills/vexa/secrets/vexa.env` manually with `VEXA_API_KEY=their_key_here`. Do not ask users to paste API keys in chat.

**Secrets location:** `skills/vexa/secrets/` holds env files and `vexa-state.json`. This dir is gitignored. When publishing the skill to ClawHub, ensure `secrets/` is excluded.
Confidence
80% 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.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
The non-interactive onboarding example hard-codes `--language en`, and several core command examples also default to English without indicating that this is only an example or that the user should choose their preferred language. Because this skill handles meeting participation and transcripts, forcing a specific language can violate locale-choice expectations unless explicitly optional or justified.

Natural-Language Policy Violations

Medium
Confidence
81% confidence
Finding
The markdown explains that the system prompt controls the bot's personality and language, but it does not instruct the operator to preserve or confirm the user's preferred language before changing it. This creates a natural-language policy risk because the skill can steer a meeting bot into a different language setting without explicit user choice.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The onboarding command explicitly passes `--language en`, which forces a specific language setting. The file does not offer the user a language/locale choice or explain why English is required, so this is a natural-language policy violation under the locale-choice rule.

Session Persistence

Medium
Category
Rogue Agent
Content
Optionally, when a meeting finishes, Vexa can POST to your OpenClaw hooks endpoint. The bundled transform processes the webhook and triggers creation of a basic meeting report.

**This setup is entirely optional.** Without it, you can create reports manually with `vexa.mjs report`. The skill never modifies `openclaw.json` automatically — all changes below are done by you.

## Setup (manual)
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.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script fetches meeting transcripts and includes a plaintext snippet from the first non-empty transcript segment in its JSON output. Meeting transcripts commonly contain sensitive business, personal, or regulated information, so exposing excerpts in routine audit output can leak data to logs, terminals, CI artifacts, or downstream tools that were not intended to handle transcript content.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This script retrieves sensitive meeting data from the Vexa API and persists it as a plaintext markdown report under the current workspace without any consent gate, warning, or minimization controls in this code path. Because transcripts, participant information, URLs, and meeting metadata can contain confidential business or personal data, running the tool in the wrong context can cause unintentional collection and local exposure of sensitive information.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
#!/usr/bin/env node
/**
 * Vexa onboarding helper:
 * 1) Capture API key and optionally persist it in skills/vexa/secrets/vexa.env (chmod 600)
 * 2) Start a bot for a test meeting
 * 3) Poll transcript briefly
 * 4) Stop bot
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Context-Inappropriate Capability

Medium
Confidence
87% confidence
Finding
The script inspects ~/.openclaw session data and configuration files, including hook mappings and tokens, to determine whether webhooks were configured or received. This is sensitive host-state access unrelated to the minimum needed for meeting onboarding, and it exposes internal application metadata and credentials to the skill, increasing the blast radius if the skill is misused or modified.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The script can read the host application's hook token from ~/.openclaw/openclaw.json and then use it to send an authenticated POST to the local OpenClaw gateway via --send-mock-webhook or the validation flow. Although framed as testing, this crosses the skill boundary and allows the skill to synthesize trusted Vexa webhook events inside the host application, which could trigger downstream ingestion, report generation, or workflow automation based on forged meeting-completed events.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The transform turns an untrusted webhook payload into operational instructions that direct the agent to modify local memory and entity files, which expands behavior beyond narrowly generating a meeting report. In an agentic environment, this creates a prompt-injection and unauthorized state-modification risk because external event data can trigger persistent changes to local knowledge stores without explicit user approval or strong validation.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The transform embeds the full raw webhook payload directly into the agent-visible message, which may expose transcripts, participant identities, meeting metadata, links, or other sensitive content to downstream model context, logs, and operators. Because webhook payloads are untrusted and potentially rich in sensitive data, indiscriminate inclusion increases both privacy exposure and prompt-injection surface.

Natural-Language Policy Violations

Medium
Confidence
84% confidence
Finding
The `bots:config:update` command requires a `--language` value and applies it directly to bot configuration, which can impose a locale/language setting without any documented opt-in from the end user affected by the bot. This matches the policy concern for language or locale constraints being enforced without an explicit user choice.

Description-Behavior Mismatch

Medium
Confidence
88% confidence
Finding
The manifest focuses on joining meetings and obtaining transcripts, recordings, and reports. Setting a user webhook URL is an account/integration-management feature that goes beyond that described scope and is not an obvious implementation detail of joining meetings.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The `user:webhook:set` command transmits a webhook URL to the Vexa service, which can affect outbound delivery behavior and may expose integration details. Although the code performs the network call directly, there is no confirmation prompt or user-facing warning here beyond basic usage text.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The manifest describes a skill for sending bots to meetings and retrieving transcripts, recordings, and reports. This file additionally exposes commands to read, set, and reset a global voice-agent system prompt, which is a separate agent-configuration capability not implied by the stated meeting-ingestion purpose.

Intent-Code Divergence

Low
Confidence
92% confidence
Finding
The file comment states its purpose is to 'create a basic meeting report,' implying the transform itself performs report creation. In reality, the code merely returns a message telling an external agent to run `vexa.mjs report`, then post-process the output and modify entity files.

Natural-Language Policy Violations

Low
Confidence
71% confidence
Finding
This is a natural-language constraint embedded in the skill behavior that forces a specific output format for chat messages. Although operationally motivated, it imposes a fixed communication policy without explicit user opt-in or broader context.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.env_credential_access

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/onboard.mjs:256

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/vexa.mjs:185

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/audit.mjs:20

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/ingest.mjs:22

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/onboard.mjs:28

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/vexa.mjs:36