Back to skill

Security audit

macOS Local Voice

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent local macOS voice tool, but its text-to-speech helper can overwrite audio files outside its intended output folder when given a custom path.

Review this before installing if you expect agents to handle untrusted instructions or custom output paths. The local voice functionality is purpose-aligned and offline, but the TTS helper should ideally restrict outputs to ~/.openclaw/media/outbound, reject traversal or absolute paths, and avoid overwriting existing files. Until fixed, prefer the default output path and do not let untrusted content choose output_path.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/tts.mjs:76
Finding
Unrestricted Output Path Permits Arbitrary File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `scripts/tts.mjs`, lines 76–105 **Vulnerability Type**: Unrestricted filesystem path and unsafe file overwrite **Risk Level**: Medium ### Vulnerable Code ```js // Prepare output path const outDir = join(process.env.HOME, ".openclaw", "media", "outbound"); mkdirSync(outDir, { recursive: true }); const basePath = outputArg || join(outDir, `tts-${Date.now()}`); const aiffPath = `${basePath}.aiff`; // Generate speech const sayArgs = []; if (voice) sayArgs.push("-v", voice); sayArgs.push("-o", aiffPath, text); try { execFileSync("say", sayArgs, { stdio: ["pipe", "pipe", "pipe"] }); } catch (e) { die(`say failed: ${((e.stderr || "") + (e.stdout || "")).trim()}`, 6); } if (!existsSync(aiffPath) || statSync(aiffPath).size === 0) { die("say produced empty output", 6); } // Convert to ogg/opus if ffmpeg available if (which("ffmpeg")) { const oggPath = `${basePath}.ogg`; try { execFileSync( "ffmpeg", ["-hide_banner", "-loglevel", "error", "-y", "-i", aiffPath, "-c:a", "libopus", "-b:a", "48k", "-vbr", "on", "-compression_level", "10", oggPath], { stdio: ["pipe", "pipe", "pipe"] }, ); ``` ### Technical Analysis The optional `outputArg` is accepted directly as the output base path. The code does not canonicalize the path, restrict it to the intended `~/.openclaw/media/outbound` directory, reject traversal or absolute paths, check for symbolic links, or require creation of a new file. The program appends `.aiff` and `.ogg` to the attacker-influenced base path and passes those paths to native utilities. In particular, `ffmpeg` is invoked with `-y`, which explicitly authorizes overwriting an existing output file without prompting. The `say` utility may likewise replace an existing AIFF output at the selected path. Using `execFileSync` with argument arrays prevents shell-command injection, but it does not prevent filesystem path manipulation or overwrite attacks. ### Attack Pa ...[truncated 1407 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve all output paths against the intended outbound directory and reject paths that escape it: ```js import { resolve, relative, isAbsolute } from "node:path"; const allowedDir = resolve(process.env.HOME, ".openclaw", "media", "outbound"); const requestedBase = outputArg ? resolve(allowedDir, outputArg) : resolve(allowedDir, `tts-${Date.now()}`); const rel = relative(allowedDir, requestedBase); if (rel.startsWith("..") || isAbsolute(rel)) { die("output path must remain inside the outbound media directory", 2); } ``` 2. Prefer accepting only a filename rather than an arbitrary path. Reject path separators, `.`/`..` components, and unexpected filename characters. 3. Do not silently overwrite existing files. Remove `ffmpeg`'s `-y` option or replace it with `-n` so conversion fails if the target already exists. 4. Generate unpredictable, application-controlled filenames using `mkdtemp`, `randomUUID`, or an equivalent secure mechanism rather than relying only on a timestamp. 5. Before writing, use `lstatSync` to reject existing symbolic links and non-regular files. Where possible, create files atomically with exclusive-create semantics. 6. Keep intermediate and final files in a newly created private directory with restrictive permissions, then return the generated path to the caller. 7. If external output locations are a required feature, require explicit trusted-user authorization and perform canonical-path, ownership, type, and overwrite checks before invoking `say` or `ffmpeg`. ]]>
Vulnerability Patterns
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (2)

Ae1

High
Category
analysis-evasion
Content
able (exit code 0, no error). **Always** use `voices.mjs check` before calling `tts.mjs` with a specific voice name.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/stt.mjs:74

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/tts.mjs:91