Back to skill

Security audit

Speak Turbo - Talk to your Claude 90ms latency!

Security checks for vulnerabilities and agentic risk

Overview

Speak-Turbo is a coherent local text-to-speech skill, but it needs review because it installs unpinned code and runs a localhost TTS daemon without authentication or resource limits.

Review before installing, especially on shared or sensitive machines. Prefer running it in an isolated environment, avoid speaking sensitive text aloud, stop the daemon when finished, do not permanently allow broad output directories, and pin or verify dependencies before using the installer.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
speakturbo/daemon_streaming.py:71
Finding
Unauthenticated Local TTS Endpoint Permits Cross-Site Resource Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `speakturbo/daemon_streaming.py:71-78, 97-118` **Vulnerability Type**: Unauthenticated local API, cross-site request exposure, and missing resource limits **Risk Level**: Medium ### Vulnerable Code ```python # DNS rebinding protection - only allow localhost @app.middleware("http") async def validate_host(request: Request, call_next): host = request.headers.get("host", "").split(":")[0] if host not in {"127.0.0.1", "localhost"}: return JSONResponse(status_code=403, content={"detail": "Forbidden"}) return await call_next(request) ``` ```python @app.get("/tts") async def tts(text: str, voice: str = "alba"): """Ultra-fast streaming TTS.""" global _last_request_time _last_request_time = time.time() if not text or not text.strip(): raise HTTPException(status_code=400, detail="Text cannot be empty") if voice not in VOICES: raise HTTPException(status_code=400, detail=f"Voice must be one of: {VOICES}") model = get_model() voice_state = get_voice_state(voice) async def generate(): yield wav_header(model.sample_rate) for chunk in model.generate_audio_stream(voice_state, text.strip()): yield (chunk.clamp(-1, 1) * 32767).short().numpy().tobytes() await asyncio.sleep(0) yield bytes(int(model.sample_rate * 0.15) * 2) # Trailing silence return StreamingResponse(generate(), media_type="audio/wav") ``` ### Technical Analysis The daemon binds to localhost, but the `/tts` endpoint does not require an authentication token and does not validate the request's `Origin` or other proof that the request came from the trusted CLI. The Host-header middleware only verifies that the request targets `127.0.0.1` or `localhost`. It provides partial DNS-rebinding protection but does not authenticate the caller. A hostile webpage can attempt to send requests directly to a loopback address. Brow ...[truncated 2283 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a cryptographically random authentication token whenever the daemon starts. 2. Store the token in a user-owned file under `~/.speakturbo/` with permissions restricted to the current user. 3. Require the token in an HTTP header, such as `Authorization: Bearer <token>`, on every endpoint. 4. Change `/tts` from GET to POST and accept text through a request body. 5. Reject browser-originated requests unless their `Origin` is explicitly trusted. Do not rely solely on CORS response headers as authentication. 6. Impose a strict maximum text length appropriate for the intended TTS workload. 7. Add per-client rate limiting and a small global limit on concurrent generations. 8. Apply request and generation timeouts, and cancel model generation when the client disconnects. 9. Continue binding only to `127.0.0.1`, while retaining Host validation as defense in depth. 10. Add tests demonstrating that missing or incorrect tokens, untrusted origins, oversized text, and excessive concurrent requests are rejected. ]]>

T08 · Insecure Dependencies

Warning
Location
install.sh:11
Finding
Installation Executes Broadly Versioned Third-Party Dependencies Without Reproducible Locks<![CDATA[ ## Vulnerability Details **File Location**: `install.sh:11-14, 20-24`; related declarations in `pyproject.toml:24-28`, `speakturbo-cli/Cargo.toml:6-11`, `README.md:31-39`, and `.github/workflows/install-test.yml:8-12` **Vulnerability Type**: Unpinned dependency installation and build-time supply-chain exposure **Risk Level**: Medium ### Vulnerable Code `install.sh:11-14`: ```bash # Install Python dependencies # Version bounds match pyproject.toml [project.dependencies] — Keep in sync echo "→ Installing Python dependencies..." pip install --quiet "pocket-tts>=0.1.0,<1.0" "uvicorn>=0.20.0,<1.0" "fastapi>=0.100.0,<1.0" "python-dateutil>=2.7,<3.0" ``` `install.sh:20-24`: ```bash if command -v cargo &> /dev/null && [ -d "$SCRIPT_DIR/speakturbo-cli" ]; then echo "→ Building Rust CLI from local source..." cd "$SCRIPT_DIR/speakturbo-cli" cargo build --release --quiet cp target/release/speakturbo ~/.local/bin/ ``` `pyproject.toml:24-28`: ```toml dependencies = [ "pocket-tts>=0.1.0", "fastapi>=0.100.0", "uvicorn>=0.20.0", "python-dateutil>=2.7", # Required by matplotlib (pocket-tts dependency) ] ``` `speakturbo-cli/Cargo.toml:6-11`: ```toml [dependencies] clap = { version = "4", features = ["derive"] } ureq = "2" rodio = { version = "0.17", default-features = false, features = ["wav"] } anyhow = "1" dirs = "5" ``` `README.md:31-39`: ```bash # For AI agents npx skills add EmZod/Speak-Turbo # CLI only pip install pocket-tts uvicorn fastapi cd speakturbo-cli && cargo build --release ``` `.github/workflows/install-test.yml:8-12`: ```yaml steps: - name: Install skills run: | npx -y skills add EmZod/speak -y || true npx -y skills add EmZod/Speak-Turbo -y || true ``` ### Technical Analysis The installer resolves and executes the latest dependency versions matching broad ranges. The repository structure provided for the audit does not include a `Cargo.lock` file or a hash-locked Python requirements file ...[truncated 2439 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create a fully resolved Python dependency lock file that pins exact direct and transitive versions. 2. Include cryptographic hashes and install with hash verification, such as `pip install --require-hashes`. 3. Include `Cargo.lock` in the repository for the Rust application and build with `cargo build --release --locked`. 4. Pin the `skills` npm command to an exact reviewed package version rather than invoking an unspecified latest version through `npx`. 5. Prefer immutable package references or verified release artifacts where supported. 6. Pin GitHub Actions to reviewed commit SHAs instead of mutable major-version or branch tags. 7. Use automated dependency update tooling so version changes are isolated, reviewed, and tested. 8. Run vulnerability and provenance checks such as `pip-audit`, `cargo audit`, and npm integrity verification in CI. 9. Build dependencies in a restricted environment without unnecessary credentials or access to sensitive host files. 10. Document that the installer must not be run as root and should preferably operate inside an isolated Python environment. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (23)

Possible Typosquatting: 'uvicorn' resembles popular package 'gunicorn'

High
Category
Supply Chain
Confidence
70% confidence
Finding
Package name closely resembles a popular package, suggesting possible typosquatting. Attackers publish malicious packages with similar names to trick developers into installing them.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
92% confidence
Finding
The README instructs users to install the skill via `npx skills add EmZod/Speak-Turbo` without pinning a specific immutable version, commit, or digest. This creates a supply-chain risk: a future compromised or malicious update to the referenced package/repository could be pulled and executed by users or agents at install time.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill documentation describes capabilities involving shell commands, local file reads/writes, and network access, but the skill declares no explicit tool scope or permissions boundary. In an agent setting, this can cause the agent to invoke higher-risk operations than a user expects, increasing the chance of unintended file modification, process management, or local service interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
- Your current working directory
- `~/.speakturbo/`

If you need to write elsewhere, use `--allow-dir`:

```bash
speakturbo "Hello" -o /custom/path/audio.wav --allow-dir /custom/path
Confidence
90% confidence
Finding
The skill documents persistent modification of ~/.speakturbo/config to permanently expand allowed output directories. This creates session persistence beyond the immediate task and weakens future path restrictions, so a one-time agent action can broaden write access for later runs without renewed user intent.

Session Persistence

Medium
Category
Rogue Agent
Content
echo "→ Installing Python dependencies..."
pip install --quiet "pocket-tts>=0.1.0,<1.0" "uvicorn>=0.20.0,<1.0" "fastapi>=0.100.0,<1.0" "python-dateutil>=2.7,<3.0"

# Create bin directory
mkdir -p ~/.local/bin

# Build Rust CLI from local source, or fall back to Python wrapper
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
);
    eprintln!("To allow it permanently, add to ~/.speakturbo/config:");
    eprintln!(
        "  mkdir -p ~/.speakturbo && echo \"{}\" >> ~/.speakturbo/config",
        parent_dir.display()
    );
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The CLI sends user-provided text to a local daemon over plain HTTP on 127.0.0.1 without any explicit disclosure at the point of use. Although loopback traffic is not exposed to the network in the usual case, sensitive text entered by the user is still being transmitted to another process, which may have different trust boundaries, logging behavior, or local interception risks on a compromised host.

Session Persistence

Medium
Category
Rogue Agent
Content
# Start streaming daemon as subprocess
    daemon_script = Path(__file__).parent / "daemon_streaming.py"
    
    # Create log directory
    log_dir = Path("/tmp")
    log_file = log_dir / "speakturbo.log"
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
log_file = log_dir / "speakturbo.log"
    
    with open(log_file, "a") as log:
        process = subprocess.Popen(
            [sys.executable, str(daemon_script)],
            stdout=log,
            stderr=log,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Session Persistence

Medium
Category
Rogue Agent
Content
f"  speakturbo \"text\" -o {output} --allow-dir {parent_dir}\n"
        f"\n"
        f"To allow it permanently, add to ~/.speakturbo/config:\n"
        f"  mkdir -p ~/.speakturbo && echo \"{parent_dir}\" >> ~/.speakturbo/config",
        file=sys.stderr,
    )
    sys.exit(1)
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
try:
            # Use afplay on macOS
            subprocess.run(["afplay", temp_path], check=True)
        except FileNotFoundError:
            # Try aplay on Linux
            try:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
except FileNotFoundError:
            # Try aplay on Linux
            try:
                subprocess.run(["aplay", temp_path], check=True)
            except FileNotFoundError:
                print(f"Audio saved to: {temp_path}", file=sys.stderr)
                print("Install afplay (macOS) or aplay (Linux) to play audio.", file=sys.stderr)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_cli(*args, input_text=None, timeout=30):
    """Run the CLI with given arguments."""
    cmd = ["python", CLI_PATH] + list(args)
    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Session Persistence

Medium
Category
Rogue Agent
Content
"""Error message should show how to permanently allow via config."""
        result = run_cli("test", "-o", "/usr/share/speakturbo_test.wav")
        assert "~/.speakturbo/config" in result.stderr
        assert "mkdir -p" in result.stderr

    def test_allow_dir_flag_overrides_block(self):
        """--allow-dir should let you write to an otherwise-blocked path."""
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Low
Confidence
89% confidence
Finding
The skill prominently instructs immediate playback and describes daemon auto-start, but does not clearly warn that using it may produce audible speaker output or launch a background process. In shared or sensitive environments, this can leak information aloud, surprise users, or leave an unexpected resident process running.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The examples show using -o to write audio files but do not explicitly warn that the target file may be created or overwritten. This can lead to accidental data loss or unintended artifact creation if an agent selects an existing path or a sensitive working directory.

Unverifiable Dependency: setuptools has 10 known advisory(ies) (CVE-2013-1633 (Setuptools vulnerable to Man-in-the-middle attacks); CVE-2025-47273 (setuptools has a path traversal vulnerability in PackageIndex.download that lead); CVE-2024-6345 (setuptools vulnerable to Command Injection via package URL) +7 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Unverifiable Dependency: fastapi has 3 known advisory(ies) (CVE-2021-32677 (Cross-Site Request Forgery (CSRF) in FastAPI); CVE-2021-32677 (FastAPI is a web framework for building APIs with Python 3.6+ based on standard ); CVE-2024-24762 (FastAPI is a web framework for building APIs with Python 3.8+ based on standard )), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Unverifiable Dependency: uvicorn has 4 known advisory(ies) (CVE-2020-7694 (Log injection in uvicorn); CVE-2020-7695 (HTTP response splitting in uvicorn); CVE-2020-7694 (This affects all versions of package uvicorn. The request logger provided by the) +1 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Unverifiable Dependency: pytest has 2 known advisory(ies) (CVE-2025-71176 (pytest has vulnerable tmpdir handling); CVE-2025-71176 (pytest has vulnerable tmpdir handling)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Missing User Warnings

Low
Confidence
76% confidence
Finding
This code file exposes a network endpoint that accepts user text and processes it into audio, but the endpoint contains no confirmation prompt and no user-facing log, print, or inline warning about that processing. For code files, SQP-2 applies when data-processing or networked operations lack any visible disclosure, and the only docstring present is a terse implementation description rather than a user warning.

Static analysis

No suspicious patterns detected.