Back to skill

Security audit

Announcer

Security checks for vulnerabilities and agentic risk

Overview

The skill appears to do what it claims, but it needs review because one configuration value can be inserted into AppleScript without validation, creating a local code-execution risk if the config is tampered with.

Review before installing. Use it only on a Mac where you trust the announcer config directory, the ANNOUNCER_WORKSPACE value, and the sibling elevenlabs skill. Keep airfoil.volume as a numeric value between 0.0 and 1.0, avoid untrusted config files, and do not announce sensitive text unless you are comfortable sending that text to ElevenLabs for processing.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/announce.py:103
Finding

AppleScript Injection Through Unvalidated Airfoil Volume Configuration

Content
View full analysis

Vulnerability Details

File Location: scripts/announce.py, lines 103–119 and 286–287
Vulnerability Type: AppleScript injection caused by unvalidated configuration data
Risk Level: High

Vulnerable Code

python
def setup_airfoil(speakers: list[str], volume: float = 0.7) -> bool:
    """Set Airfoil source to System-Wide Audio, connect speakers, and set volume."""
    speaker_list = '", "'.join(_sanitize_applescript_string(s) for s in speakers)
    script = f'''
    tell application "Airfoil"
        set current audio source to system source "System-Wide Audio"
        set targetSpeakers to {{"{speaker_list}"}}
        repeat with s in (every speaker)
            if (name of s) is in targetSpeakers then
                connect to s
                set (volume of s) to {volume}
            else
                disconnect from s
            end if
        end repeat
        return "ok"
    end tell
    '''
    result = subprocess.run(
        ["osascript", "-e", script],
        capture_output=True,
        text=True,
        timeout=30
    )
    return result.returncode == 0

The value reaches this function directly from the JSON configuration:

python
volume = config["airfoil"].get("volume", 0.7)
if not setup_airfoil(speakers, volume):
    print("Airfoil setup failed", file=sys.stderr)
    sys.exit(1)

Technical Analysis

The airfoil.volume value is loaded from config.json without checking that it is a finite numeric value in the documented range of 0.0 through 1.0. Python type annotations do not enforce runtime types, so JSON can supply a string instead of a number.

The value is then interpolated directly into executable AppleScript source:

python
set (volume of s) to {volume}

Speaker names are passed through _sanitize_applescript_string, but no equivalent validation is applied to volume. An attacker who can create o ...[truncated 2110 chars]

Remediation
View remediation

Remediation Suggestions

  1. Validate the value immediately after loading the configuration. Accept only JSON numbers, explicitly reject strings and booleans, and enforce the documented range:

    python
    import math
    
    raw_volume = config.get("airfoil", {}).get("volume", 0.7)
    
    if isinstance(raw_volume, bool) or not isinstance(raw_volume, (int, float)):
        raise ValueError("airfoil.volume must be a number")
    
    volume = float(raw_volume)
    if not math.isfinite(volume) or not 0.0 <= volume <= 1.0:
        raise ValueError("airfoil.volume must be finite and between 0.0 and 1.0")
    
  2. Keep validation at the execution boundary as defense in depth. setup_airfoil() should validate volume again rather than relying exclusively on its caller.

  3. Serialize the validated value using a fixed numeric representation before inserting it into AppleScript:

    python
    safe_volume = format(volume, ".6f")
    

    Only safe_volume, derived from an already validated numeric value, should be added to the script.

  4. Add configuration-schema validation for all externally controlled fields, including speaker arrays, voice identifiers, audio formats, chime paths, timeouts, and volume.

  5. Treat ANNOUNCER_WORKSPACE as a trust boundary. If the environment may be attacker-influenced, restrict the workspace to an approved absolute directory and verify configuration-file ownership and permissions before loading it.

  6. Add regression tests proving that string, boolean, infinite, non-finite, negative, and greater-than-one volume values are rejected before osascript is invoked.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (10)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill advertises executable behavior that depends on environment variables and shell-capable script execution, but it does not declare an explicit tool scope such as permissions or allowed-tools. This weakens trust boundaries and reviewability, because an agent may invoke shell or access secrets without the skill manifest clearly constraining or disclosing those capabilities.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The usage and behavior description does not clearly warn users that announcement text is transmitted to ElevenLabs for cloud TTS generation. This creates a privacy and data-handling risk, because users may submit sensitive household messages under the assumption that processing is local when the content is actually sent to a third party.

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/announce.py (reported line 79)May include surrounding context.

python
"-o", output_path,
        "--format", fmt
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"TTS error: {result.stderr}", file=sys.stderr)
        return False

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/announce.py (reported line 97)May include surrounding context.

python
"-o", output_path,
        "--format", fmt
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"TTS error: {result.stderr}", file=sys.stderr)
        return False

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
88% confidence
Finding

This call executes dynamically constructed AppleScript via osascript, embedding speaker names and a config-derived volume directly into the script. Speaker names are partially sanitized, but the numeric volume is not validated before interpolation, so a malicious or tampered config could inject arbitrary AppleScript tokens and cause unintended local automation actions.

Content

Scanner excerpt · scripts/announce.py (reported line 119)May include surrounding context.

python
return "ok"
    end tell
    '''
    result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True, timeout=30)
    return result.returncode == 0

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/announce.py (reported line 146)May include surrounding context.

python
last_status = None
    
    while time.time() - start < timeout:
        result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True, timeout=5)
        if result.returncode == 0:
            try:
                # Parse output: ["Speaker1|true", "Speaker2|false", ...]

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/announce.py (reported line 179)May include surrounding context.

python
last_status = None
    
    while time.time() - start < timeout:
        result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True, timeout=5)
        if result.returncode == 0:
            try:
                # Parse output: ["Speaker1|true", "Speaker2|false", ...]

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/announce.py (reported line 207)May include surrounding context.

python
def play_audio(path: str):
    """Play audio through Terminal (picked up by Airfoil)."""
    subprocess.run(["afplay", path])


def disconnect_all():

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/announce.py (reported line 219)May include surrounding context.

python
end repeat
    end tell
    '''
    subprocess.run(["osascript", "-e", script], capture_output=True)


def list_speakers(as_json: bool = False):

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/announce.py (reported line 233)May include surrounding context.

python
return speakerList
    end tell
    '''
    result = subprocess.run(["osascript", "-e", script], capture_output=True, text=True, timeout=10)
    if result.returncode != 0:
        print("Error: Could not query Airfoil. Is it running?", file=sys.stderr)
        sys.exit(1)

Static analysis

No suspicious patterns detected.