Back to skill

Security audit

tts

Security checks for vulnerabilities and agentic risk

Overview

This TTS skill mostly does what it claims, but it has under-disclosed cloud uploads and credential-handling paths that need review before installation.

Install only if you are comfortable with text, subtitles, and voice-reference audio being sent to Noiz by default unless you explicitly force Kokoro. Avoid using untrusted --base-url values or untrusted reference-audio URLs, clear .tmp/tts after sensitive renders, and treat the Noiz API key as exposed if the skill is run with arguments supplied by an untrusted workflow.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/render_timeline.py:366
Finding

API Credential Disclosure Through an Attacker-Controlled API Origin

Content
View full analysis
str: import requests resp = requests.post( f"{base_url.rstrip('/')}/emotion-enhance", headers={"Authorization": api_key, "Content-Type": "application/json"}, json={"text": text}, timeout=timeout, ) ``` ```python def _noiz_tts( base_url: str, api_key: str, cue: Cue, cfg: Dict[str, Any], output_format: str, timeout: int, out_path: Path, ) -> float: import requests url = f"{base_url.rstrip('/')}/text-to-speech" # ... resp = requests.post( url, headers={"Authorization": api_key}, data=payload, files=files, timeout=timeout, ) ``` ```python ap.add_argument("--api-key", help="API key (required for noiz backend)") ap.add_argument("--output", required=True, help="Output audio file") ap.add_argument("--base-url", default="https://noiz.ai/v1") ``` The main wrapper also forwards its loaded credential and all unrecognized render arguments: ```python render_argv = ["--srt", args.srt, "--voice-map", args.voice_map, "--output", args.output, "--backend", backend] if api_key: render_argv += ["--api-key", api_key] render_argv += extra ``` ```python parser = build_parser() # parse_known_args: extra unknown args are forwarded to render_timeline.py args, extra = parser.parse_known_args(argv) ``` ### Technical Analysis The timeline renderer accepts an unrestricted `--base-url` and attaches the Noiz API key to requests sent to that origin. It does not require HTTPS or verify that the destination hostname is `noiz.ai`. The primary `tts.py` entry point makes this more dangerous: it obtain ...[truncated 1580 chars]
Remediation
View remediation

other

Warning
Location
scripts/tts.py:103
Finding

Default TTS Operation Unexpectedly Uploads User Text to a Cloud Service

Content
View full analysis
str: if explicit: return explicit return "noiz" if load_api_key() else "noiz-guest" ``` ```python elif backend == "noiz-guest": ensure_noiz_ready() if not args.voice_id: print( "Error: --voice-id is required in guest mode (no API key configured).", file=sys.stderr, ) # ... return 1 print( "[noiz-guest] Using guest mode (limited features, no API key required)", file=sys.stderr, ) from noiz_tts import synthesize_guest as _noiz_guest_synthesize text = args.text if not text and args.text_file: text = Path(args.text_file).read_text(encoding="utf-8").strip() _noiz_guest_synthesize( base_url="https://noiz.ai/v1", text=text, voice_id=args.voice_id, output_format=fmt, speed=args.speed or 1.0, timeout=120, out_path=Path(output), ) ``` The Skill documentation describes Kokoro as the default and states: ```markdown | Just read text aloud, no fuss | Kokoro (default) | ``` It also states that no data is sent unless a Noiz command is invoked. The implementation instead chooses authenticated Noiz when a key exists and Noiz guest mode otherwise. ### Technical Analysis Backend selection is cloud-first rather than local-first. A caller who omits `--backend` does not receive the documented offline Kokoro behavior. Instead, the supplied text or complete text-file contents are submitted to a Noiz network endpoint. Network access is legitimate when the user explicitly chooses cloud synthesis. The security issue is the mismatch between declared behavior and actual default behavior, which prevents ...[truncated 899 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/render_timeline.py:224
Finding

Arbitrary Reference-Audio URLs Permit SSRF and Unbounded Downloads

Content
View full analysis
str: """Download ref audio if URL; return local path.""" if ref_audio_input.startswith("http://") or ref_audio_input.startswith("https://"): import urllib.request tmp = mktemp_suffixed(".wav") print( "[noiz] Downloading reference audio: {}".format(ref_audio_input), file=sys.stderr, ) urllib.request.urlretrieve(ref_audio_input, str(tmp)) return str(tmp) return ref_audio_input ``` The timeline renderer contains a second unrestricted downloader: ```python def _resolve_reference_audio(ref: str, timeout: int) -> Tuple[Path, Optional[Path]]: """Resolve reference_audio to a path. If ref is a URL, download to temp file. Returns (path_to_use, temp_path_to_cleanup_or_None).""" if ref.startswith("http://") or ref.startswith("https://"): import requests tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) tmp.close() r = requests.get(ref, timeout=timeout) r.raise_for_status() Path(tmp.name).write_bytes(r.content) return Path(tmp.name), Path(tmp.name) p = Path(ref) if not p.exists(): raise FileNotFoundError(f"reference_audio not found: {ref}") return p, None ``` The downloaded content is subsequently opened and sent as multipart data: ```python if ref: ref_path, ref_cleanup = _resolve_reference_audio(ref, timeout) files = { "file": ( ref_path.name, ref_path.open("rb"), "application/octet-stream", ) } resp = requests.post( url, headers={"Authorization": api_key}, data=payload, files=files, timeout=timeout, ) `` ...[truncated 1942 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/render_timeline.py:385
Finding

Timeline Rendering Retains Sensitive Intermediate Audio and Metadata

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (19)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

This code’s primary purpose is subtitle/timeline generation from text, not text-to-speech. It converts text into an .srt file by sentence splitting and estimated durations based on characters-per-second, then writes that file to disk. There is no audio synthesis, no speech model invocation, no voice output, no alignment to existing media, and no cloning or expressive speech controls. While subtitle-related timing could be adjacent to TTS workflows, the declared description is specifically about producing spoken audio, and this code does not do that. Filesystem access is consistent; network access is over-declared but not itself a mismatch under the stated criteria.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
75% confidence
Finding

The skill uses 'file_read' capability that is not listed in its permissions. This may indicate deceptive intent or missing permission declarations.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
75% confidence
Finding

The skill uses 'file_write' capability that is not listed in its permissions. This may indicate deceptive intent or missing permission declarations.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
88% confidence
Finding

The script executes local binaries (ffmpeg, ffprobe, kokoro-tts) via subprocess, which is effectively shell/process-execution capability, but the declared permissions only list network and filesystem. This creates a permission-model mismatch that can undermine sandboxing and user expectations, especially in an agent skill where execution capabilities should be explicit.

Content

No source excerpt is available for this finding.

Lp1

High
Category
MCP Least Privilege
Confidence
75% confidence
Finding

The skill uses 'env' capability that is not listed in its permissions. This may indicate deceptive intent or missing permission declarations.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

Overly broad triggers such as 'speak', 'say', and 'voice' can cause the skill to activate for unrelated requests, which is risky because this skill has network and filesystem permissions. Unintended invocation could lead to unexpected file reads, audio generation, or data being sent to the Noiz backend when the user did not intend to use this capability.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

Repeating ambiguous activation terms in the trigger section reinforces a broad matching policy that may spuriously invoke the skill. In the context of a skill with network access and optional upload of reference audio, accidental activation increases the chance of unintended data handling or external transmission.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

This skill sends user-provided text and optional reference audio to remote Noiz API endpoints, including potentially sensitive document contents and biometric voice data, without a clear runtime disclosure or confirmation. In the context of a TTS skill with network and filesystem permissions, this creates a real privacy and data-handling risk because users may assume processing is local while their content is uploaded externally.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/render_timeline.py (reported line 116)May include surrounding context.

python
def _run_ff(cmd: List[str]) -> None:
    proc = subprocess.run(cmd, capture_output=True, text=True)
    if proc.returncode != 0:
        raise RuntimeError(f"ffmpeg failed: {' '.join(cmd)}\n{proc.stderr}")

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/render_timeline.py (reported line 340)May include surrounding context.

python
def _run_ff(cmd: List[str]) -> None:
    proc = subprocess.run(cmd, capture_output=True, text=True)
    if proc.returncode != 0:
        raise RuntimeError(f"ffmpeg failed: {' '.join(cmd)}\n{proc.stderr}")

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

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

python
def probe_duration_ms(path: Path) -> float:
    proc = subprocess.run(
        [
            "ffprobe", "-v", "error", "-show_entries", "format=duration",
            "-of", "default=noprint_wrappers=1:nokey=1", str(path),

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The auto-emotion feature sends cue text to a separate remote endpoint before synthesis, causing additional external disclosure beyond the main TTS request. Because subtitle text may contain private or copyrighted content, this extra transfer meaningfully increases privacy risk if users are not explicitly informed at the point of use.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

When the noiz backend is used, subtitle text and optional reference audio are transmitted to a remote service, and reference_audio may even be fetched from arbitrary URLs first. In a TTS skill, this can expose sensitive document contents, subtitles, or biometric voice samples to third parties without an explicit, contextual warning or consent checkpoint.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

Beyond TTS generation, the code migrates, reads, normalizes, stores, and permission-hardens API credentials under the user's home directory. Credential management may support the backend, but it is a separate operational capability not described in the manifest's user-facing scope.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

The manifest describes converting text and related sources into spoken audio, but this function also invokes local media player programs to immediately play the generated file. Spawning external executables is a separate host-interaction capability not clearly justified by the stated purpose of generating audio artifacts.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

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

python
for player in ("afplay", "aplay", "paplay"):
        if shutil.which(player):
            print("[tts] Playing audio...", file=sys.stderr)
            subprocess.call([player, path])
            return
    print(
        "[tts] No audio player found (tried afplay, aplay, paplay). "

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

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

python
cmd += ["--speed", str(args.speed)]

        try:
            subprocess.check_call(cmd)
        finally:
            unlink_silent(tmp_input)

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
74% confidence
Finding

The documentation states that the default --cps value of 4 is 'good for Chinese,' which bakes in a language-specific default without asking the user to choose a locale or reading speed. While mild, this can steer behavior toward a specific language assumption rather than offering an explicit language or locale choice.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

This code triggers a real network request to the Noiz guest API and writes the returned audio to disk. Although the file-level docstring and class docstring mention a live integration test and real API usage, the test body itself has no confirmation prompt or runtime disclosure before carrying out those actions.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.