Back to skill

Security audit

Fish Tts

Security checks for vulnerabilities and agentic risk

Overview

The skill’s TTS-and-upload purpose is coherent, but it exposes a NextCloud password and sends user content and Basic auth over unencrypted HTTP.

Review before installing or running as-is. Rotate the exposed NextCloud password, remove hard-coded credential defaults, require user-supplied secrets, use HTTPS for TTS and NextCloud, avoid sending sensitive text or audio unless you trust those services, and fix the temporary-file and upload-command inconsistencies.

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

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.py:15
Finding

Hard-Coded NextCloud Credentials Transmitted over Plaintext HTTP

Content
View full analysis

Vulnerability Details

File Location: SKILL.py:15-18, SKILL.py:100-109, SKILL.md:29-35, SKILL.md:210-216
Vulnerability Type: Hard-coded secret and plaintext credential transmission
Risk Level: High

Vulnerable Code

SKILL.py:15-18:

python
NEXTCLOUD_USER = os.environ.get('NEXTCLOUD_USER', 'openclaw')
NEXTCLOUD_PASS = os.environ.get('NEXTCLOUD_PASS', 'N95qg-Wzdpc-6DJAn-xMaHa-RaEW5')
NEXTCLOUD_URL = os.environ.get('NEXTCLOUD_URL', 'http://192.168.68.68:8080')
FISH_AUDIO_S1_URL = os.environ.get('FISH_AUDIO_S1_URL', 'http://192.168.68.78:7860')

SKILL.py:100-109:

python
with open(audio_file_path, 'rb') as f:
    nextcloud_url = f"{NEXTCLOUD_URL}/remote.php/webdav/Openclaw/{filename}"
    
    response = requests.put(
        nextcloud_url,
        auth=(NEXTCLOUD_USER, NEXTCLOUD_PASS),
        timeout=120
    )

SKILL.md:29-35:

bash
export NEXTCLOUD_USER="openclaw"
export NEXTCLOUD_PASS="N95qg-Wzdpc-6DJAn-xMaHa-RaEW5"
export NEXTCLOUD_URL="http://192.168.68.68:8080"
export FISH_AUDIO_S1_URL="http://192.168.68.78:7860"

SKILL.md:210-216:

bash
# Configuration
NEXTCLOUD_USER="${NEXTCLOUD_USER:-openclaw}"
NEXTCLOUD_PASS="${NEXTCLOUD_PASS:-N95qg-Wzdpc-6DJAn-xMaHa-RaEW5}"
NEXTCLOUD_URL="${NEXTCLOUD_URL:-http://192.168.68.68:8080}"
FISH_AUDIO_S1_URL="${FISH_AUDIO_S1_URL:-http://192.168.68.78:7860}"

Technical Analysis

A reusable NextCloud username and password are embedded in both executable code and documentation. When the relevant environment variables are absent, the Python implementation silently falls back to these exposed credentials.

The configured NextCloud URL uses unencrypted HTTP. The requests.put call applies HTTP Basic authentication through the auth parameter. Basic authentication only encodes credentials and does not encrypt them. Without TLS, a network-positioned attacker can observe the Authorization hea ...[truncated 1464 chars]

Remediation
View remediation

Remediation Suggestions

  1. Immediately rotate the exposed NextCloud password and invalidate any active credentials or application tokens derived from it.

  2. Remove the credential from executable code, documentation, examples, repository history, packaged artifacts, and logs.

  3. Do not provide a secret as an environment-variable fallback. Require explicit secret configuration and fail closed when it is absent:

    python
    NEXTCLOUD_USER = os.environ.get("NEXTCLOUD_USER")
    NEXTCLOUD_PASS = os.environ.get("NEXTCLOUD_PASS")
    NEXTCLOUD_URL = os.environ.get("NEXTCLOUD_URL")
    
    if not all((NEXTCLOUD_USER, NEXTCLOUD_PASS, NEXTCLOUD_URL)):
        raise RuntimeError("Required NextCloud configuration is missing")
    
  4. Store credentials in a dedicated secret manager, operating-system credential store, or securely scoped runtime secret injection mechanism.

  5. Require an https:// NextCloud endpoint with certificate verification enabled. Do not provide an HTTP fallback.

  6. Prefer a revocable, narrowly scoped application password rather than the user's primary password.

  7. Restrict the service account to the minimum required directory and operations.

  8. Add automated secret scanning and configuration checks to prevent credentials and plaintext service URLs from being committed again.

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.py:129
Finding

Predictable Temporary Files Permit Local Symlink-Based File Overwrite

Content
View full analysis

Vulnerability Details

File Location: SKILL.py:70-73, SKILL.py:129-136, SKILL.py:202-220
Vulnerability Type: Predictable and insecure temporary-file handling
Risk Level: Medium

Vulnerable Code

SKILL.py:70-73:

python
if response.status_code == 200:
    with open(output_path, 'wb') as f:
        f.write(response.content)
        print(f"✅ Audio saved to {output_path}")

SKILL.py:129-136:

python
# Create timestamp for filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

# Generate output filename
if output_filename:
    filename = output_filename
else:
    filename = f"fish_tts_{timestamp}.mp3"

# Create temporary file path
temp_path = f"/tmp/fish_tts_{timestamp}.mp3"

SKILL.py:202-220:

python
# Check Kokoro TTS
kokoro_result = generate_audio(KOKORO_TTS_URL, "Health check", "em_alex", "/tmp/kokoro_health.mp3")
if Path("/tmp/kokoro_health.mp3").exists():
    results["kokoro_tts"] = {"status": "working", "service": "Kokoro TTS"}
else:
    results["kokoro_tts"] = {"status": "error", "service": "Kokoro TTS"}

# Check Fish Audio S1
fish_result = generate_audio(FISH_AUDIO_S1_URL, "Health check", "af_bella", "/tmp/fish_health.mp3")
if Path("/tmp/fish_health.mp3").exists():
    results["fish_audio_s1"] = {"status": "error", "service": "Fish Audio S1"}
else:
    results["fish_audio_s1"] = {"status": "working", "service": "Fish Audio S1"}

# Check OpenVoice V2
openvoice_result = generate_audio(OPENVOICE_V2_URL, "Health check", "af_bella", "/tmp/openvoice_health.mp3")
if Path("/tmp/openvoice_health.mp3").exists():
    results["openvoice_v2"] = {"status": "error", "service": "OpenVoice V2"}
else:
    results["openvoice_v2"] = {"status": "working", "service": "OpenVoice V2"}

Technical Analysis

The application creates temporary files under the shared /tmp directory using either timestamp-derived names or fix ...[truncated 1907 chars]

Remediation
View remediation

Remediation Suggestions

  1. Use Python's tempfile module to atomically create files with unpredictable names and restrictive permissions:

    python
    import tempfile
    
    with tempfile.NamedTemporaryFile(
        prefix="fish_tts_",
        suffix=".mp3",
        delete=False
    ) as temp_file:
        temp_path = temp_file.name
        temp_file.write(response.content)
    
  2. Ensure temporary files are created with permissions equivalent to 0600.

  3. Avoid fixed health-check paths. Create a distinct secure temporary file for every service check.

  4. Delete temporary audio in a finally block after upload or health-check processing:

    python
    try:
        # Generate and process the temporary audio.
        pass
    finally:
        Path(temp_path).unlink(missing_ok=True)
    
  5. Where supported, use no-follow and exclusive-creation semantics to prevent symbolic-link traversal and pre-existing-file replacement.

  6. Consider using a private, permission-restricted runtime directory rather than the globally shared /tmp directory.

  7. Do not infer health solely from whether a previously used path exists; inspect the current request result and securely remove stale files before or after each check.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (13)

Tainted flow: 'nextcloud_url' from os.environ.get (line 93, credential/environment) → requests.put (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 · SKILL.py (reported line 95)May include surrounding context.

python
with open(audio_file_path, 'rb') as f:
            nextcloud_url = f"{NEXTCLOUD_URL}/remote.php/webdav/Openclaw/{filename}"
            
            response = requests.put(
                nextcloud_url,
                auth=(NEXTCLOUD_USER, NEXTCLOUD_PASS),
                timeout=120

Missing User Warnings

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The skill hard-codes live NextCloud credentials directly in documentation and script defaults, exposing secrets to anyone who can read the file. This can lead to unauthorized access, data theft, file tampering, or reuse of the same credentials elsewhere, and the use of plain HTTP further increases the chance of credential interception.

Content

No source excerpt is available for this finding.

Missing User Warnings

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

A hardcoded NextCloud password is embedded directly in the source, which exposes a reusable secret to anyone with code access and often leads to credential leakage through repos, backups, logs, or packaging. Because the script also uses that credential for network authentication, compromise can directly enable unauthorized access to the remote storage service.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The skill encourages sending user-provided text to a TTS service and automatically uploading generated audio to NextCloud without clearly warning that content leaves the local context and is stored remotely. This creates privacy and data-handling risk, especially if users submit sensitive prompts, personal data, or confidential material assuming processing is local-only.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
85% confidence
Finding

This command transmits user-supplied text to a network service over HTTP, which creates confidentiality risk and expands the trust boundary beyond the local skill. In this skill's context, external transmission is expected functionality, but it is still security-relevant because sensitive text could be exposed to the service operator or intercepted in transit.

Content

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

Generate speech from text:

bash
curl -s -X POST http://192.168.68.78:7860/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"fish", "text":"Hello from Fish Audio S1!", "voice":"em_michael"}' \
  -o /tmp/fish_audio.mp3

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The documentation specifies uploads to .../remote.php/webdav/..., but the script uploads to $NEXTCLOUD_URL/Openclaw/... without the documented WebDAV path. Because the docs explicitly state WebDAV is used for file operations, this is a direct intent/implementation mismatch rather than mere incompleteness.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The help text says upload will 'Upload existing MP3 file to NextCloud' and provides an example using $0 upload /path/to/file.mp3, but the upload) case only prints that upload requires a generated file and returns an error. This is an active contradiction between the documented interface and the actual command behavior.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
84% confidence
Finding

This example repeats the same behavior of sending text to the TTS endpoint over HTTP, so the same confidentiality and privacy concerns apply. Although the transmission is core to the skill's purpose, the lack of encryption and warning makes it a real exposure rather than a false positive.

Content

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

Generate Greeting (Testing)

bash
curl -s -X POST http://192.168.68.78:7860/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model":"fish", "text":"Hello! This is a test of the Fish Audio S1 TTS skill for OpenClaw.", "voice":"em_michael"}' \
  -o /tmp/fish_audio_test.mp3

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SKILL.py (reported line 59)May include surrounding context.

python
start_time = time.time()
        
        response = requests.post(
            f"{tts_url}/api/v1/tts",
            headers={"Content-Type": "application/json"},
            timeout=60,

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The generate_audio function sends the provided text to an HTTP endpoint, which may expose user content over the network. While the code logs that audio generation is occurring, it does not clearly disclose to users that their input text is being transmitted to a remote service.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The upload_to_nextcloud function performs a network upload of a local audio file using WebDAV credentials. Although it prints an upload status message, the script does not clearly warn users in advance that generated or selected files will be sent to NextCloud, which can affect privacy and data handling expectations.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The surrounding comments and structure indicate this block is intended to report whether each TTS service is working. However, unlike the Kokoro check, the Fish Audio S1 and OpenVoice V2 branches label the service as "error" when the generated health-check file exists and "working" when it does not, which is the opposite of the documented intent of a health check.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
77% confidence
Finding

The default test phrase is hardcoded in English, and the CLI/help text presents the tool around English-language output without any explicit language or locale choice. This can be a natural-language policy concern when a skill defaults to a single language without user opt-in or justification.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.