Back to skill

Security audit

elevenlabs-voices

Security checks for vulnerabilities and agentic risk

Overview

This skill is mostly a straightforward ElevenLabs audio tool, but its API-key storage claims and batch file-output handling need review before installation.

Install only after reviewing the credential handling. Prefer environment variables or a secure secret manager instead of running setup.py, avoid committing config.json or .env, rotate any key that may have been saved or shared, and process only trusted batch JSON until output-path validation is fixed. Expect submitted text, sound prompts, voice-design descriptions, preview text, and the API key to be sent to ElevenLabs over HTTPS.

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

Warning
Location
scripts/tts.py:400
Finding

Batch TTS Output Path Traversal Permits Filesystem Writes Outside the Output Directory

Content
View full analysis

Vulnerability Details

File Location: scripts/tts.py:400-413, with the file-write sink in scripts/tts.py:327-335
Vulnerability Type: Unvalidated batch output path / path traversal
Risk Level: Medium

Vulnerable Code

python
for i, text in enumerate(texts, 1):
    # Handle dict entries with custom voice/output
    if isinstance(text, dict):
        t = text.get("text", "")
        v = text.get("voice", voice_name)
        o = text.get("output", f"output_{i:04d}.mp3")
    else:
        t = str(text)
        v = voice_name
        o = f"output_{i:04d}.mp3"
    
    output_file = out_path / o
    print(f"  [{i}/{len(texts)}] Processing: {t[:50]}...")
    
    if synthesize(t, v, str(output_file), voices_data, api_key, language, False, pronunciations):
        success += 1

The resulting path reaches this write operation:

python
with urllib.request.urlopen(req, timeout=30) as response:
    audio_data = response.read()
    
    # Write to file
    with open(output_path, "wb") as f:
        f.write(audio_data)

Technical Analysis

The output property in a batch JSON entry is used as a filesystem path without checking whether it is absolute, contains parent-directory traversal components, or resolves outside the requested output directory.

Joining out_path with an absolute path can discard the intended base directory. Relative values such as ../../target.mp3 can similarly escape it after path resolution. The destination is opened with wb, so an existing user-writable file is truncated and replaced.

The bytes written are audio returned by ElevenLabs rather than arbitrary executable content. Nevertheless, the operation violates least filesystem privilege because batch generation only requires writing inside the selected output directory.

Attack Path

  1. An attacker creates or modifies a TTS batch JSON file that a victim will process.
  2. The attacker supplies an output property such as:
json
[
  {
    "text": "At
...[truncated 1011 chars]
Remediation
View remediation

Remediation Suggestions

  1. Reject absolute output paths supplied by batch entries.
  2. Resolve the candidate path and verify that it remains beneath the resolved output directory:
python
base_dir = Path(output_dir).resolve()
base_dir.mkdir(parents=True, exist_ok=True)

output_name = Path(o)
if output_name.is_absolute():
    raise ValueError("Absolute output paths are not permitted")

output_file = (base_dir / output_name).resolve()
if output_file != base_dir and base_dir not in output_file.parents:
    raise ValueError("Output path escapes the output directory")
  1. If nested output folders are unnecessary, accept only a basename:
python
output_file = base_dir / Path(o).name
  1. Restrict allowed output extensions to expected audio formats.
  2. Create any permitted parent directories only after containment validation.
  3. Consider exclusive file creation or an explicit overwrite option so existing files are not silently truncated.
  4. Document that batch files should be treated as untrusted input and add tests covering absolute paths, .. traversal, and symbolic-link edge cases.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/sfx.py:201
Finding

Batch Sound-Effect Output Path Traversal Permits Filesystem Writes Outside the Output Directory

Content
View full analysis

Vulnerability Details

File Location: scripts/sfx.py:201-210, with the file-write sink in scripts/sfx.py:129-135
Vulnerability Type: Unvalidated batch output path / path traversal
Risk Level: Medium

Vulnerable Code

python
for i, item in enumerate(items, 1):
    if isinstance(item, str):
        item = {"prompt": item, "output": f"sfx_{i:04d}.mp3"}
    
    prompt = item.get("prompt", "")
    duration = item.get("duration")
    output_name = item.get("output", f"sfx_{i:04d}.mp3")
    output_file = out_path / output_name
    
    print(f"  [{i}/{len(items)}] {prompt[:50]}...")
    
    if generate_sfx(prompt, str(output_file), duration, 0.3, api_key):
        success += 1

The path reaches this write operation:

python
with urllib.request.urlopen(req, timeout=60) as response:
    audio_data = response.read()
    
    with open(output_path, "wb") as f:
        f.write(audio_data)

Technical Analysis

The batch entry's output value is attacker-controlled and is joined directly to out_path. The code does not reject absolute paths, parent-directory traversal, or destinations that resolve outside the selected output directory.

Because the destination is opened in wb mode, a successful sound-generation response can overwrite an existing user-writable file. This exceeds the minimum filesystem access required for the declared batch sound-effect functionality.

Attack Path

  1. An attacker supplies a sound-effect batch file containing a traversal or absolute destination:
json
[
  {
    "prompt": "A short alert tone",
    "duration": 1,
    "output": "../../outside-sfx-directory.mp3"
  }
]
  1. The victim processes it:
bash
python3 scripts/sfx.py --batch attacker.json --output-dir ./sfx
  1. output_name is joined to the output directory without validation.
  2. The ElevenLabs request succeeds and returns audio.
  3. The script opens the escaped destination in wb mode.
  4. The destination is created or overwri ...[truncated 554 chars]
Remediation
View remediation

Remediation Suggestions

  1. Treat batch output names as untrusted input.
  2. Reject absolute paths and canonicalize destinations before writing.
  3. Verify that every resolved destination remains inside the resolved output directory:
python
base_dir = Path(output_dir).resolve()
base_dir.mkdir(parents=True, exist_ok=True)

relative_name = Path(output_name)
if relative_name.is_absolute():
    raise ValueError("Absolute output paths are not permitted")

output_file = (base_dir / relative_name).resolve()
if output_file != base_dir and base_dir not in output_file.parents:
    raise ValueError("Output path escapes the output directory")
  1. Prefer sanitized basenames if subdirectories are not an intended feature.
  2. Allow only expected audio extensions and reject empty filenames.
  3. Require an explicit overwrite option or use exclusive creation to prevent silent truncation.
  4. Add automated tests for absolute paths, Unix and Windows traversal forms, nested paths, and symbolic-link escapes.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/setup.py:126
Finding

Setup Wizard Stores the ElevenLabs API Key in an Unused Plaintext Configuration File

Content
View full analysis

Vulnerability Details

File Location: scripts/setup.py:126-128 and scripts/setup.py:205-208
Vulnerability Type: Plaintext credential storage and misleading secret-protection guarantees
Risk Level: Medium

Vulnerable Code

The setup wizard collects and retains the API key in the configuration object:

python
while True:
    api_key = get_input("Enter your ElevenLabs API key", required=True)
    if validate_api_key(api_key):
        config['apiKey'] = api_key
        print(f"{Colors.GREEN}  ✓ API key accepted{Colors.RESET}")
        break

It then serializes the complete configuration, including the key, to a normal JSON file:

python
def save_config(config: dict, path: Path):
    """Save configuration to JSON file."""
    with open(path, 'w') as f:
        json.dump(config, f, indent=2)
    print(f"\n{Colors.GREEN}✓ Configuration saved to: {path}{Colors.RESET}")

The setup interface claims protection that is not present in the audited project:

python
print(f"{Colors.GREEN}🔒 Privacy: Your API key is stored locally only.{Colors.RESET}")
print(f"{Colors.GREEN}   It never leaves your machine and is in .gitignore.{Colors.RESET}")

The runtime scripts instead obtain credentials from environment variables or the local .env file. For example, scripts/tts.py:208-219 contains:

python
api_key = os.environ.get("ELEVEN_API_KEY") or os.environ.get("ELEVENLABS_API_KEY")
if api_key:
    return api_key

# Try skill-local .env file
env_file = SKILL_DIR / ".env"
if env_file.exists():
    for line in env_file.read_text().splitlines():
        if line.startswith("ELEVEN_API_KEY="):
            return line.split("=", 1)[1].strip().strip('"\'')

Technical Analysis

The wizard writes the complete API key into config.json using ordinary file creation. It does not explicitly enforce owner-only permissions such as mode 0600. Effective permissions therefore depend on the user's environment and umask.

No .gitignore file exi ...[truncated 1894 chars]

Remediation
View remediation

Remediation Suggestions

  1. Do not place API keys in config.json. Store only non-sensitive preferences there.
  2. Use environment variables or an operating-system credential store for the API key.
  3. Make the setup wizard configure the same credential source that runtime scripts actually consume.
  4. If local plaintext storage remains supported, create the secret file with owner-only permissions:
python
import os

fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w") as f:
    json.dump(config, f, indent=2)
  1. Add a repository .gitignore containing at least:
gitignore
config.json
.env
.usage.json
samples/
batch_output/
sfx_output/
  1. Warn when an existing configuration file has broader permissions than intended.
  2. Remove existing unnecessary API keys from config.json during migration and instruct users to rotate keys that may have been committed or shared.
  3. Update the privacy documentation to state that submitted credentials, TTS text, sound prompts, voice descriptions, and preview text are transmitted to ElevenLabs over HTTPS.
  4. Avoid examples that embed API keys directly in configuration files when an environment-backed secret reference is available.
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (47)

Tainted flow: 'req' from os.environ.get (line 124, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/sfx.py (reported line 131)May include surrounding context.

python
if duration:
            print(f"   Duration: {duration}s")
        
        with urllib.request.urlopen(req, timeout=60) as response:
            audio_data = response.read()
            
            with open(output_path, "wb") as f:

Tainted flow: 'req' from os.environ.get (line 318, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/tts.py (reported line 323)May include surrounding context.

python
try:
        if stream:
            # Streaming mode - write chunks as they arrive
            with urllib.request.urlopen(req, timeout=60) as response:
                with open(output_path, "wb") as f:
                    total_bytes = 0
                    while True:

Tainted flow: 'req' from os.environ.get (line 318, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/tts.py (reported line 336)May include surrounding context.

python
print()
                print(f"✅ Saved: {output_path} ({total_bytes / 1024:.1f} KB)")
        else:
            with urllib.request.urlopen(req, timeout=30) as response:
                audio_data = response.read()
                
                # Write to file

Tainted flow: 'req' from os.environ.get (line 179, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/voice-design.py (reported line 123)May include surrounding context.

python
print(f"   Description: {description[:50]}...")
    
    try:
        with urllib.request.urlopen(req, timeout=60) as response:
            # Response includes audio and voice ID
            content_type = response.headers.get("Content-Type", "")

Tainted flow: 'req' from os.environ.get (line 179, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/voice-design.py (reported line 182)May include surrounding context.

python
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
    
    try:
        with urllib.request.urlopen(req, timeout=30) as response:
            result = json.loads(response.read().decode("utf-8"))
            saved_voice_id = result.get("voice_id")
            print(f"✅ Voice saved to library!")

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The skill explicitly documents network use and local file operations but does not declare them in permissions metadata. Even if some feature-mismatch claims are not provable from this file alone, the omission of declared network and file access is a real operational security issue because it weakens user consent and sandboxing expectations.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The skill explicitly documents network use and local file operations but does not declare them in permissions metadata. Even if some feature-mismatch claims are not provable from this file alone, the omission of declared network and file access is a real operational security issue because it weakens user consent and sandboxing expectations.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
88% confidence
Finding

The skill explicitly documents network use and local file operations but does not declare them in permissions metadata. Even if some feature-mismatch claims are not provable from this file alone, the omission of declared network and file access is a real operational security issue because it weakens user consent and sandboxing expectations.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The skill explicitly documents network use and local file operations but does not declare them in permissions metadata. Even if some feature-mismatch claims are not provable from this file alone, the omission of declared network and file access is a real operational security issue because it weakens user consent and sandboxing expectations.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 213)May include surrounding context.

md
python3 scripts/sfx.py --prompt "Thunder rumbling in the distance"

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 216)May include surrounding context.

md
python3 scripts/sfx.py --prompt "Thunder rumbling in the distance"

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 219)May include surrounding context.

md
python3 scripts/sfx.py --prompt "Thunder rumbling in the distance"

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 222)May include surrounding context.

md
python3 scripts/sfx.py --prompt "Thunder rumbling in the distance"

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 225)May include surrounding context.

md
python3 scripts/sfx.py --prompt "Thunder rumbling in the distance"

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 457)May include surrounding context.

md
python3 scripts/sfx.py --prompt "Thunder rumbling in the distance"

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · SKILL.md (reported line 224)May include surrounding context.

Batch SFX generation

python3 scripts/sfx.py --batch sounds.json --output-dir ./sfx

Show prompt examples

python3 scripts/sfx.py --examples

text

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · scripts/sfx.py (reported line 295)May include surrounding context.

python
# Batch SFX generation
python3 scripts/sfx.py --batch sounds.json --output-dir ./sfx

# Show prompt examples
python3 scripts/sfx.py --examples
```

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/sfx.py (reported line 33)May include surrounding context.

python
def get_api_key() -> str:
    """Get API key from environment or OpenClaw config."""
    api_key = os.environ.get("ELEVEN_API_KEY") or os.environ.get("ELEVENLABS_API_KEY")
    if api_key:
        return api_key

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/tts.py (reported line 208)May include surrounding context.

python
def get_api_key() -> str:
    """Get API key from environment or OpenClaw config."""
    api_key = os.environ.get("ELEVEN_API_KEY") or os.environ.get("ELEVENLABS_API_KEY")
    if api_key:
        return api_key

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/voice-design.py (reported line 49)May include surrounding context.

python
def get_api_key() -> str:
    """Get API key from environment or OpenClaw config."""
    api_key = os.environ.get("ELEVEN_API_KEY") or os.environ.get("ELEVENLABS_API_KEY")
    if api_key:
        return api_key

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/sfx.py (reported line 38)May include surrounding context.

python
if api_key:
        return api_key
    
    env_file = SKILL_DIR / ".env"
    if env_file.exists():
        for line in env_file.read_text().splitlines():
            if line.startswith("ELEVEN_API_KEY="):

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/voice-design.py (reported line 54)May include surrounding context.

python
if api_key:
        return api_key
    
    env_file = SKILL_DIR / ".env"
    if env_file.exists():
        for line in env_file.read_text().splitlines():
            if line.startswith("ELEVEN_API_KEY="):

Credential Access

High
Category
Privilege Escalation
Confidence
80% confidence
Finding

Reading API credentials from a skill-local .env file can expose secrets through weak filesystem permissions, accidental inclusion in archives, or check-in to source control. In this skill context the intent is convenience, not theft, but local secret files are a real secret-management weakness.

Content

Scanner excerpt · scripts/tts.py (reported line 214)May include surrounding context.

python
if api_key:
        return api_key
    
    # Try skill-local .env file
    env_file = SKILL_DIR / ".env"
    if env_file.exists():
        for line in env_file.read_text().splitlines():

Credential Access

High
Category
Privilege Escalation
Confidence
80% confidence
Finding

The existence check and subsequent parsing of a local .env file are part of the same secret-handling weakness: credentials may be stored in an insecure location adjacent to the skill. This is not active credential theft, but it increases the chance of unintended disclosure.

Content

Scanner excerpt · scripts/tts.py (reported line 215)May include surrounding context.

python
return api_key
    
    # Try skill-local .env file
    env_file = SKILL_DIR / ".env"
    if env_file.exists():
        for line in env_file.read_text().splitlines():
            if line.startswith("ELEVEN_API_KEY="):

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 383)May include surrounding context.

md
print("   Options:")
    print("   1. Set ELEVEN_API_KEY environment variable")
    print("   2. Configure in OpenClaw (tts.elevenlabs.apiKey)")
    print("   3. Create .env file in skill directory")
    sys.exit(1)

Static analysis

No suspicious patterns detected.