Back to skill

Security audit

Kokoro Agent Voices

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent local text-to-speech skill, but its optional playback feature can execute unintended shell commands through the output filename.

Review before installing or using. The TTS purpose is legitimate and most behavior is disclosed, but avoid --play and do not pass untrusted output paths unless the playback code is fixed to use a non-shell subprocess call or equivalent safe audio playback. Also consider whether a first-run Hugging Face model download without a pinned revision fits your environment's supply-chain requirements.

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

Error
Location
scripts/speak.py:127
Finding

Shell Command Injection Through the Output Path

Content
View full analysis

Vulnerability Details

File Location: scripts/speak.py:127
Vulnerability Type: OS command injection
Risk Level: High

Vulnerable code:

python
if args.play and out:
    os.system(f"afplay {out}")

Technical Analysis

The --output command-line argument is accepted without validation and passed through speak() as output. If audio generation succeeds, speak() returns this same path as out. When --play is enabled, the application interpolates out directly into a command executed by os.system().

os.system() invokes a command shell. Consequently, shell metacharacters in the output path—such as command separators, substitutions, or redirections—are interpreted as shell syntax rather than as part of a filename. An attacker who can influence the program arguments can therefore append arbitrary commands.

Attack Path

  1. The attacker causes the Skill to be invoked with --play and a malicious --output value containing shell syntax.
  2. The supplied value reaches speak(..., output=args.output).
  3. If synthesis produces audio, sf.write() writes the WAV and speak() returns the attacker-controlled path.
  4. Line 127 constructs afplay {out} and passes it to the shell through os.system().
  5. The shell parses the metacharacters and executes the appended command.

For example, an output argument conceptually shaped like audio.wav; id > /tmp/proof # would result in the appended id command being interpreted by the shell after successful synthesis.

Impact Assessment

Successful exploitation provides arbitrary command execution with the privileges of the user running the Skill. The attacker could read or modify files accessible to that user, execute installed programs, access user-level credentials or data, make network connections, or establish user-level persistence. The vulnerability does not itself elevate privileges beyond those of the invoking process, but its sco ...[truncated 52 chars]

Remediation
View remediation

Remediation Suggestions

Replace shell-based execution with an argument-vector subprocess call:

python
import subprocess

if args.play and out:
    subprocess.run(["afplay", out], check=True)

Passing arguments as a list prevents the output path from being parsed as shell syntax. Do not use shell=True.

Additional hardening should include:

  • Resolve the destination with Path and enforce any intended output-directory restrictions.
  • Reject unsupported output extensions or invalid path forms where appropriate.
  • Handle subprocess.CalledProcessError and missing-player errors without falling back to shell execution.
  • Add regression tests using paths containing spaces, semicolons, command substitutions, quotes, and leading hyphens to verify they are treated only as filenames.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (7)

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Confidence
98% confidence
Finding

The script passes a user-influenced file path directly into a shell command via os.system(f"afplay {out}"). Because --output is attacker-controlled and no quoting or argument separation is used, shell metacharacters in the path can trigger command injection and execute arbitrary local commands when --play is used.

Content

Scanner excerpt · scripts/speak.py (reported line 127)May include surrounding context.

python
out = speak(args.text, voice=voice, speed=speed, output=args.output)

    if args.play and out:
        os.system(f"afplay {out}")

if __name__ == "__main__":
    main()

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill advertises executable usage via python3 scripts/speak.py and implicitly requires shell and file-read capabilities, but it does not declare any explicit tool scope such as permissions or allowed-tools. This creates a governance gap: an agent or runtime may execute the skill with broader-than-necessary privileges, making it harder to enforce least privilege or review what the skill is allowed to do.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
78% confidence
Finding

The predefined agent profiles map roles to specific voices such as 'British male' or 'Warm female', and the voice naming scheme also encodes locale/language variants. This imposes voice/language characteristics by default rather than offering an explicit user choice or opt-in, which can violate language/locale policy expectations.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The voice-listing path downloads assets from Hugging Face despite the skill being described as local TTS. This creates an implicit network dependency and trust boundary: running the skill may contact external infrastructure, leak metadata, or pull unpinned remote content that changes over time.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

Speech generation instantiates KPipeline with a remote Hugging Face repo_id, so core functionality depends on fetching or trusting externally hosted model artifacts. In a skill marketed as local, this can surprise operators, weaken reproducibility, and expose users to supply-chain or privacy risks if remote content is updated or monitored.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The skill's stated purpose is generating WAV output with voice profiles, but the optional --play feature executes a platform-specific shell command via os.system. Spawning a shell is not an obvious requirement for TTS generation and adds capability beyond the manifest's described scope.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
85% confidence
Finding

The script conditionally invokes os.system to play the generated file, which is a subprocess/shell execution. While the --play flag indicates playback behavior, there is no user-facing warning, confirmation, or explanatory comment/docstring specifically disclosing that a shell command will be executed.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.