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## 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
## 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## 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
## 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
Content
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
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Missing User Warnings

High
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.

Missing User Warnings

High
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.

Missing User Warnings

Medium
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.

External Transmission

Medium
Category
Data Exfiltration
Content
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
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.

Intent-Code Divergence

Medium
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.

Intent-Code Divergence

Medium
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.

External Transmission

Medium
Category
Data Exfiltration
Content
### 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
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.

External Transmission

Medium
Category
Data Exfiltration
Content
start_time = time.time()
        
        response = requests.post(
            f"{tts_url}/api/v1/tts",
            headers={"Content-Type": "application/json"},
            timeout=60,
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
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.

Missing User Warnings

Medium
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.

Intent-Code Divergence

Medium
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.

Natural-Language Policy Violations

Low
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.

Static analysis

No suspicious patterns detected.