Back to skill

Security audit

Sonos Announce

Security checks for vulnerabilities and agentic risk

Overview

This Sonos audio skill appears purpose-aligned, but it starts a local web server and uses unsafe shell/process controls that can expose files or run/kill commands if misused.

Review this skill before installing. Use it only in a trusted local network, keep media_dir limited to a dedicated non-sensitive audio folder, avoid passing user-controlled paths, and be aware it may leave an unauthenticated local HTTP server running and may kill other processes using the configured port. Prefer a hardened version that uses subprocess without shell=True, serves only the requested file, binds explicitly, cleans up in a finally block, and pins dependencies.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
sonos_core.py:73
Finding
Shell Command Injection Through the media_dir Parameter## Vulnerability Details **File Location**: `sonos_core.py:73-81` **Vulnerability Type**: OS command injection **Risk Level**: High ### Vulnerable Code ```python if platform.system() == "Windows": # Start server and save PID to file os.system(f'start /b python -m http.server {HTTP_PORT} --directory "{media_dir}"') # On Windows, we can't easily get the PID of start /b, so just track by port # The stop_http_server will use port-based killing as fallback else: # Use nohup to ensure it persists and save PID os.system(f'nohup python3 -m http.server {HTTP_PORT} --directory "{media_dir}" > /tmp/sonos_http.log 2>&1 &') ``` ### Technical Analysis The `media_dir` argument is interpolated directly into commands executed through `os.system()`. Although it is surrounded by double quotes, the value is not escaped for the relevant command shell. A crafted value containing a closing quote and shell control operators can terminate the intended argument and append arbitrary commands. This issue affects both the Windows and Unix-like execution paths. The function is reachable through the public `announce(..., media_dir=...)` API. Consequently, any party capable of influencing that argument may be able to execute commands with the privileges of the process running the Skill. ### Attack Path 1. An attacker gains control over, or influences, the `media_dir` value supplied to `announce()`. 2. `announce()` passes the value to `start_http_server(media_dir)`. 3. `start_http_server()` embeds the untrusted value into a shell command. 4. A crafted quote closes the `--directory` argument, while shell metacharacters append another command. 5. `os.system()` invokes the platform shell and executes the injected command. 6. The injected command runs under the account and permissions of the Agent or Skill host. ### Impact Assessment Successful exploitation permits arbitrary local command execution. The att ...[truncated 260 chars]
Remediation
## Remediation Suggestions - Replace `os.system()` with `subprocess.Popen()` using an argument array and without `shell=True`. - Resolve `media_dir` with `pathlib.Path.resolve()` and require it to be an existing directory. - Restrict serving to an explicitly approved media root and reject paths that escape that root. - Track the returned `Popen` object or PID directly instead of discovering the process through shell commands. - Open log files through Python and pass their handles to `stdout` and `stderr`. Example hardened approach: ```python from pathlib import Path import subprocess directory = Path(media_dir).expanduser().resolve() if not directory.is_dir(): raise ValueError("Invalid media directory") process = subprocess.Popen( [ "python3", "-m", "http.server", str(HTTP_PORT), "--bind", HTTP_HOST, "--directory", str(directory), ], stdout=log_file, stderr=subprocess.STDOUT, start_new_session=True, ) ```

T05 · Unauthorized Access and Privilege Escalation

Error
Location
sonos_core.py:65
Finding
Unauthenticated Directory Exposure and Lingering HTTP Server## Vulnerability Details **File Location**: `sonos_core.py:65-82`, with lifecycle behavior at `sonos_core.py:399-447` **Vulnerability Type**: Excessive file exposure and persistent network service **Risk Level**: High ### Vulnerable Code ```python def start_http_server(media_dir=None): """Start the HTTP server for streaming audio to Sonos.""" if media_dir is None: media_dir = os.path.expanduser("~/.local/share/openclaw/media/outbound") # Always kill any stale server first, then start fresh print("Stopping any existing HTTP server...") stop_http_server() time.sleep(1) print(f"Starting HTTP server from {media_dir}...") if platform.system() == "Windows": # Start server and save PID to file os.system(f'start /b python -m http.server {HTTP_PORT} --directory "{media_dir}"') else: # Use nohup to ensure it persists and save PID os.system(f'nohup python3 -m http.server {HTTP_PORT} --directory "{media_dir}" > /tmp/sonos_http.log 2>&1 &') ``` The server is started by `announce()` but is not stopped before the function returns: ```python # Ensure HTTP server is running start_http_server(media_dir) # ... print("=== Done ===") return { 'coordinators': len(coordinators), 'states': states, } ``` ### Technical Analysis Python's standard `http.server` serves the complete directory supplied through `--directory`, including directory listings, and does not provide authentication or authorization. No mechanism limits access to the single announcement file. The server is also launched as a detached background process. On Unix-like systems, `nohup` explicitly allows it to survive after the invocation completes. `announce()` does not call `stop_http_server()` after playback and does not use a `finally` block to guarantee cleanup after errors. `SONOS_HTTP_HOST` is used to construct the URL sent to Sono ...[truncated 1341 chars]
Remediation
## Remediation Suggestions - Copy or link only the requested audio file into a newly created, permission-restricted temporary directory. - Use a custom HTTP request handler that disables directory listings and permits access only to the expected file. - Generate a cryptographically random, single-use URL path rather than exposing the original file name. - Bind explicitly to the required LAN interface with `--bind` or an equivalent server API. - Start the server immediately before playback and terminate it in a `finally` block after playback or restoration. - Avoid `nohup` and other detached execution unless persistent serving is an explicitly documented requirement. - Validate that `media_dir` remains beneath an approved media root. - Document the temporary LAN exposure in `SKILL.md`.

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
sonos_core.py:91
Finding
Forceful Termination of Unrelated Processes Using the Configured Port## Vulnerability Details **File Location**: `sonos_core.py:91-122` **Vulnerability Type**: Unsafe process management and denial of service **Risk Level**: Medium ### Vulnerable Code ```python def stop_http_server(): """Stop the HTTP server gracefully using PID file, then fallback to port.""" # Try PID file first (cleanest) if os.path.exists(PID_FILE): try: with open(PID_FILE, 'r') as f: pid = f.read().strip() if pid: os.kill(int(pid), 9) print(f"Stopped HTTP server (PID {pid})") except: pass try: os.remove(PID_FILE) except: pass # Fallback: kill by port if platform.system() == "Windows": try: result = subprocess.run( f'for /f "tokens=5" %a in (\'netstat -aon ^| findstr :{HTTP_PORT} ^| findstr LISTENING\') do taskkill /f /pid %a', shell=True, capture_output=True, text=True ) except: pass else: os.system(f"lsof -ti:{HTTP_PORT} | xargs kill -9 2>/dev/null") os.system(f"pkill -9 -f 'python3 -m http.server {HTTP_PORT}' 2>/dev/null") ``` ### Technical Analysis The fallback logic identifies processes by listening port or a broad command-line pattern rather than verifying that a process was created by the current Skill invocation. On Unix-like systems, every process returned by `lsof -ti:{HTTP_PORT}` is sent `SIGKILL`. The `pkill -f` fallback may also terminate unrelated Python HTTP servers whose command lines match the pattern. On Windows, any listening process found through `netstat` for the configured port is forcefully terminated with `taskkill /f`. Even the PID-file path uses `SIGKILL` without validating process identity or first attempting graceful termination. Stale PID reuse could cause an unrelated process to be ki ...[truncated 1021 chars]
Remediation
## Remediation Suggestions - Retain the exact `subprocess.Popen` object or securely recorded PID for the server created by the Skill. - Before terminating a stored PID, verify process ownership, creation time, and executable or command identity. - Attempt graceful shutdown with `terminate()` or `SIGTERM`, wait with a timeout, and use force only as a final fallback. - Never kill an arbitrary process merely because it occupies the desired port. - If the port is already in use by an unrelated service, return a clear error or select an available configured port. - Store runtime PID data in a user-private runtime directory with restrictive permissions and protect against stale PID reuse. - Remove the broad `pkill -f` and port-wide `lsof | xargs kill` fallbacks.

T08 · Insecure Dependencies

Note
Location
SKILL.md:9
Finding
Unpinned Third-Party SoCo Dependency## Vulnerability Details **File Location**: `SKILL.md:9-12`, with installation instructions at `SKILL.md:31-38` **Vulnerability Type**: Uncontrolled dependency resolution **Risk Level**: Low ### Vulnerable Code ```yaml "requires": { "bins": ["python3", "ffprobe"], "pip": ["soco"], }, ``` The installation documentation likewise installs the package without a version or integrity constraint: ```bash pip install soco ``` ### Technical Analysis The Skill requests `soco` without pinning a reviewed release or providing an integrity hash. Installation therefore resolves whichever release is current at installation time. This makes deployments non-reproducible and allows future upstream changes to alter the effective code executed by the Skill without changes to the audited project. The package name itself is consistent with the declared Sonos functionality, and no dependency-confusion or typosquatting indicator was identified. The risk arises from uncontrolled future dependency resolution rather than evidence that the current package is malicious. ### Attack Path 1. A user or deployment system follows the metadata or documentation and runs an unpinned installation. 2. The package index resolves the latest available `soco` release rather than a specifically reviewed version. 3. A compromised, malicious, or unexpectedly incompatible future release is downloaded. 4. Dependency installation or import executes code in the Skill environment. 5. That code receives the permissions and network access available to the Agent process. ### Impact Assessment A compromised dependency could execute arbitrary Python code under the Agent account, access files available to that account, communicate over the network, or manipulate Sonos operations. Without evidence of an actually compromised release, this is a preventive supply-chain finding with low current severity.
Remediation
## Remediation Suggestions - Pin `soco` to a reviewed exact version in both metadata and installation documentation. - Maintain a lock file containing transitive dependency versions. - Use hash verification, such as `pip install --require-hashes -r requirements.txt`. - Install dependencies in an isolated virtual environment with only the permissions required for Sonos control. - Periodically review and deliberately update the pinned dependency after security testing. - Use trusted package indexes over authenticated TLS and avoid adding unreviewed alternate indexes.
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (28)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The stated purpose focuses on Sonos playback and restoration, but the documentation also reveals hidden operational behaviors: starting/killing background HTTP servers, process management, IP autodetection, and serving local files over the network. This mismatch is dangerous because users may authorize a media skill without realizing it exposes local content and manipulates OS processes.

Ae1

High
Category
analysis-evasion
Content
- `SKILL.md` - This documentation
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Content
print(f"Starting HTTP server from {media_dir}...")
    if platform.system() == "Windows":
        # Start server and save PID to file
        os.system(f'start /b python -m http.server {HTTP_PORT} --directory "{media_dir}"')
        # On Windows, we can't easily get the PID of start /b, so just track by port
        # The stop_http_server will use port-based killing as fallback
    else:
Confidence
98% confidence
Finding
This builds a shell command with a user-controllable path component (media_dir) and executes it via os.system on Windows. Even though HTTP_PORT is cast to int, media_dir is only quoted, not safely escaped for cmd.exe semantics, so a crafted directory value could break out of quoting and trigger arbitrary command execution.

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Content
# The stop_http_server will use port-based killing as fallback
    else:
        # Use nohup to ensure it persists and save PID
        os.system(f'nohup python3 -m http.server {HTTP_PORT} --directory "{media_dir}" > /tmp/sonos_http.log 2>&1 &')
        # Save PID for clean shutdown
        time.sleep(1)
        # Find the PID by port and save it
Confidence
99% confidence
Finding
This invokes a shell with a formatted command containing media_dir and HTTP_PORT. On Unix-like systems, shell metacharacters in media_dir can lead to arbitrary command execution, and the command is also used to launch a persistent background service, increasing abuse potential.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
time.sleep(1)
        # Find the PID by port and save it
        try:
            pid = subprocess.check_output(f"lsof -ti:{HTTP_PORT}", shell=True).decode().strip()
            with open(PID_FILE, 'w') as f:
                f.write(pid)
        except:
Confidence
90% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Content
except:
            pass
    else:
        os.system(f"lsof -ti:{HTTP_PORT} | xargs kill -9 2>/dev/null")
        os.system(f"pkill -9 -f 'python3 -m http.server {HTTP_PORT}' 2>/dev/null")
Confidence
85% confidence
Finding
os.system() and os exec-family calls run shell commands with the process's full privileges, enabling arbitrary command execution.

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Content
pass
    else:
        os.system(f"lsof -ti:{HTTP_PORT} | xargs kill -9 2>/dev/null")
        os.system(f"pkill -9 -f 'python3 -m http.server {HTTP_PORT}' 2>/dev/null")


def is_external_input(uri):
Confidence
85% confidence
Finding
os.system() and os exec-family calls run shell commands with the process's full privileges, enabling arbitrary command execution.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill documents capabilities including environment-variable use, filesystem access, network exposure, and shell/process control, but it does not declare any explicit tool scope or permissions. In an agent setting, this increases the risk of overbroad execution because users and orchestrators cannot clearly constrain what the skill may do before running it.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The manifest description says the skill 'skips Line-In/TV/Bluetooth,' implying those source types are excluded from playback-state handling. However, later documentation states Line-In, TV/HDMI, and Bluetooth are detected as non-pausable inputs and then restored by reconnecting to them, which is materially different behavior.

Session Persistence

Medium
Category
Rogue Agent
Content
- User wants to play an announcement on Sonos
- Soundboard effects (airhorn, rimshot, etc.)
- Any audio playback that should resume previous state

**This skill handles playback only** - audio generation (TTS, ElevenLabs, etc.) is separate.
Confidence
55% 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
sys.path.insert(0, '/path/to/sonos-announce')
from sonos_core import announce

# Play audio and restore previous state
# Assumes audio is in default media_dir (~/.local/share/openclaw/media/outbound)
result = announce('my_audio.mp3')
```
Confidence
55% 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
sys.path.insert(0, '/path/to/sonos-announce')
from sonos_core import announce

# Play audio and restore previous state
# Assumes audio is in default media_dir (~/.local/share/openclaw/media/outbound)
result = announce('my_audio.mp3')
```
Confidence
55% 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
91% confidence
Finding
The documentation describes running an HTTP server in the background and controlling processes, but it does not clearly warn users that local media may be exposed over the LAN or that background processes may persist or be terminated. In context, this is more dangerous because the skill is intended for home-network use and may serve sensitive local audio files to other devices on the network.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The file's top-level description says the skill 'skips Line-In/TV/Bluetooth,' but the state restoration table explicitly documents that Line-In, TV/HDMI, and Bluetooth are re-connected after playback. This is an active contradiction in the skill's own documentation about what happens to those input types.

Context-Inappropriate Capability

Medium
Confidence
87% confidence
Finding
The skill's functionality extends beyond simple Sonos playback by starting a local web server and force-killing processes, including with broad port- and pattern-based termination. In an agent setting, that extra host-level control increases blast radius and can be abused to interfere with unrelated local services or expose local media over HTTP.

Tainted flow: 'HTTP_PORT' from os.environ.get (line 39, credential/environment) → os.system (code execution)

Medium
Category
Data Flow
Content
print(f"Starting HTTP server from {media_dir}...")
    if platform.system() == "Windows":
        # Start server and save PID to file
        os.system(f'start /b python -m http.server {HTTP_PORT} --directory "{media_dir}"')
        # On Windows, we can't easily get the PID of start /b, so just track by port
        # The stop_http_server will use port-based killing as fallback
    else:
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Session Persistence

Medium
Category
Rogue Agent
Content
# On Windows, we can't easily get the PID of start /b, so just track by port
        # The stop_http_server will use port-based killing as fallback
    else:
        # Use nohup to ensure it persists and save PID
        os.system(f'nohup python3 -m http.server {HTTP_PORT} --directory "{media_dir}" > /tmp/sonos_http.log 2>&1 &')
        # Save PID for clean shutdown
        time.sleep(1)
Confidence
65% 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
# On Windows, we can't easily get the PID of start /b, so just track by port
        # The stop_http_server will use port-based killing as fallback
    else:
        # Use nohup to ensure it persists and save PID
        os.system(f'nohup python3 -m http.server {HTTP_PORT} --directory "{media_dir}" > /tmp/sonos_http.log 2>&1 &')
        # Save PID for clean shutdown
        time.sleep(1)
Confidence
65% 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.

Tainted flow: 'HTTP_PORT' from os.environ.get (line 39, credential/environment) → os.system (code execution)

Medium
Category
Data Flow
Content
# The stop_http_server will use port-based killing as fallback
    else:
        # Use nohup to ensure it persists and save PID
        os.system(f'nohup python3 -m http.server {HTTP_PORT} --directory "{media_dir}" > /tmp/sonos_http.log 2>&1 &')
        # Save PID for clean shutdown
        time.sleep(1)
        # Find the PID by port and save it
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
time.sleep(1)
        # Find the PID by port and save it
        try:
            pid = subprocess.check_output(f"lsof -ti:{HTTP_PORT}", shell=True).decode().strip()
            with open(PID_FILE, 'w') as f:
                f.write(pid)
        except:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'HTTP_PORT' from os.environ.get (line 39, credential/environment) → subprocess.check_output (code execution)

Medium
Category
Data Flow
Content
time.sleep(1)
        # Find the PID by port and save it
        try:
            pid = subprocess.check_output(f"lsof -ti:{HTTP_PORT}", shell=True).decode().strip()
            with open(PID_FILE, 'w') as f:
                f.write(pid)
        except:
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if platform.system() == "Windows":
        # Use netstat to find and kill the process on the port
        try:
            result = subprocess.run(
                f'for /f "tokens=5" %a in (\'netstat -aon ^| findstr :{HTTP_PORT} ^| findstr LISTENING\') do taskkill /f /pid %a',
                shell=True, capture_output=True, text=True
            )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'HTTP_PORT' from os.environ.get (line 39, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
if platform.system() == "Windows":
        # Use netstat to find and kill the process on the port
        try:
            result = subprocess.run(
                f'for /f "tokens=5" %a in (\'netstat -aon ^| findstr :{HTTP_PORT} ^| findstr LISTENING\') do taskkill /f /pid %a',
                shell=True, capture_output=True, text=True
            )
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'HTTP_PORT' from os.environ.get (line 39, credential/environment) → os.system (code execution)

Medium
Category
Data Flow
Content
except:
            pass
    else:
        os.system(f"lsof -ti:{HTTP_PORT} | xargs kill -9 2>/dev/null")
        os.system(f"pkill -9 -f 'python3 -m http.server {HTTP_PORT}' 2>/dev/null")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Tainted flow: 'HTTP_PORT' from os.environ.get (line 39, credential/environment) → os.system (code execution)

Medium
Category
Data Flow
Content
pass
    else:
        os.system(f"lsof -ti:{HTTP_PORT} | xargs kill -9 2>/dev/null")
        os.system(f"pkill -9 -f 'python3 -m http.server {HTTP_PORT}' 2>/dev/null")


def is_external_input(uri):
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Static analysis

No suspicious patterns detected.