Back to skill

Security audit

Radarr+

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its Radarr movie-request purpose, but it includes an overbroad downloader that can fetch any URL and overwrite any writable path, plus weak credential-transport guidance.

Review before installing. Use only with trusted chat allowlists, prefer HTTPS Radarr/Plex URLs, avoid optional Plex unless you intend that credential use, and do not let untrusted input control fetch_asset.py URLs or output paths. Expect the skill to store movie tracking state and chat targets locally for progress notifications.

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
scripts/fetch_asset.py:20
Finding
Unrestricted Remote Asset Retrieval and Arbitrary File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch_asset.py:20-32` **Vulnerability Type**: Unrestricted URL retrieval, SSRF, and arbitrary file overwrite **Risk Level**: High ### Vulnerable Code ```python def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(prog="fetch_asset") ap.add_argument("--url", required=True) ap.add_argument("--out", required=True) args = ap.parse_args(argv) out_path = pathlib.Path(args.out) out_path.parent.mkdir(parents=True, exist_ok=True) req = urllib.request.Request(args.url, headers={"User-Agent": "openclaw-radarr-skill"}) with urllib.request.urlopen(req, timeout=60) as resp: data = resp.read() out_path.write_bytes(data) ``` ### Technical Analysis The script accepts an unrestricted source URL and unrestricted destination path. Although the documented use is to retrieve a poster from TMDB and place it under an outbound directory, the implementation does not enforce either constraint. `urllib.request.urlopen()` can access arbitrary network destinations and follows HTTP redirects. There is no validation of the URL scheme, hostname, resolved IP address, redirect destination, response content type, or response size. This can allow server-side request forgery against loopback, private-network, link-local, or cloud metadata services reachable from the Agent host. The output path is also used directly. `Path.write_bytes()` truncates and overwrites an existing file if the process has permission. There is no resolved-path containment check, symlink protection, exclusive file creation, or restriction to the documented outbound directory. The response is read entirely into memory without a size limit and can then consume local disk space. These capabilities exceed the minimum privileges required to download a TMDB poster. ### Attack Path 1. An attacker supplies or causes the Agent to use a crafted asset URL instead of a legitimate TMDB image URL. 2. The Agent i ...[truncated 1173 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict downloads to HTTPS and an explicit hostname allowlist, such as `image.tmdb.org`. 2. Resolve hostnames and reject loopback, private, link-local, multicast, reserved, and unspecified IP addresses. 3. Disable redirects or validate every redirect destination using the same scheme, host, and IP restrictions. 4. Reject URLs containing embedded credentials and unsupported ports. 5. Resolve the destination path and verify that it remains under a dedicated directory such as `outbound/radarr/`. 6. Reject absolute paths, parent-directory traversal, and symlinks. 7. Use exclusive file creation or an explicit safe-overwrite policy. 8. Stream the response while enforcing a conservative maximum byte count. 9. Require an expected image content type and validate the downloaded file as an image before saving it. 10. Apply restrictive file permissions and remove partially downloaded files when validation fails. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/radarr.py:34
Finding
API Credentials Can Be Transmitted over Plaintext HTTP and Exposed in URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/radarr.py:34-61`; `scripts/radarr_status.py:39-51`; `scripts/radarr_track.py:39-56`; `scripts/plex_link.py:40-52`; `SKILL.md:30-41`; `references/setup.md:22-35`; `references/onboarding.md:42-53` **Vulnerability Type**: Insecure credential transport and credential-bearing query strings **Risk Level**: Medium ### Vulnerable Code Radarr accepts the configured URL without enforcing HTTPS and sends the API key in an authentication header: ```python def _base_url() -> str: url = _env("RADARR_URL").rstrip("/") return url def _api_key() -> str: return _env("RADARR_API_KEY") def _request(path: str, *, method: str = "GET", params: dict | None = None, body: dict | None = None): base = _base_url() url = base + path if params: qs = urllib.parse.urlencode(params) url = url + ("?" if "?" not in url else "&") + qs headers = { "X-Api-Key": _api_key(), "Accept": "application/json", } data = None if body is not None: data = json.dumps(body).encode("utf-8") headers["Content-Type"] = "application/json" req = urllib.request.Request(url, method=method, headers=headers, data=data) try: with urllib.request.urlopen(req, timeout=30) as resp: ``` Plex similarly accepts an unrestricted base URL and places its token in the query string: ```python def _get_xml(path: str, params: dict | None = None) -> ET.Element | None: base = _plex_base() token = _token() if not base or not token: return None url = base + path q = dict(params or {}) q["X-Plex-Token"] = token url = url + "?" + urllib.parse.urlencode(q) req = urllib.request.Request(url, headers={"Accept": "application/xml"}) with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read().decode("utf-8", errors="replace") ``` The documentation explicitly encourages plaintext examples: ```text RADARR_URL= ...[truncated 2401 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require `https://` for Radarr and Plex endpoints by default. 2. If plaintext HTTP is necessary for localhost or a trusted isolated LAN, require an explicit opt-in setting and display a clear warning. 3. Validate URL schemes during startup and reject embedded credentials, fragments, and unexpected schemes. 4. Configure TLS certificate verification and document the use of a trusted internal certificate authority rather than disabling verification. 5. Avoid placing the Plex token in the query string where Plex compatibility permits authentication through a request header. 6. Ensure credential-bearing URLs are redacted from errors, logs, telemetry, and diagnostics. 7. Disable redirects for authenticated calls or require redirects to remain HTTPS and on the exact configured origin. 8. Use narrowly scoped service credentials where supported, rotate existing credentials after any suspected plaintext exposure, and restrict service access through firewall rules. 9. Update all documentation examples to use HTTPS and place HTTP configuration in a clearly marked exceptional-case section. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (31)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill performs queueing and local state/outbox management for messaging targets, but that operational behavior is not front-and-center in the declared purpose. In practice, undeclared message-queue and tracking behavior can leak chat identifiers, create persistent records, and enable unintended notifications or workflow abuse if users believe they are only invoking a Radarr API wrapper.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill performs queueing and local state/outbox management for messaging targets, but that operational behavior is not front-and-center in the declared purpose. In practice, undeclared message-queue and tracking behavior can leak chat identifiers, create persistent records, and enable unintended notifications or workflow abuse if users believe they are only invoking a Radarr API wrapper.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The skill performs queueing and local state/outbox management for messaging targets, but that operational behavior is not front-and-center in the declared purpose. In practice, undeclared message-queue and tracking behavior can leak chat identifiers, create persistent records, and enable unintended notifications or workflow abuse if users believe they are only invoking a Radarr API wrapper.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The skill performs queueing and local state/outbox management for messaging targets, but that operational behavior is not front-and-center in the declared purpose. In practice, undeclared message-queue and tracking behavior can leak chat identifiers, create persistent records, and enable unintended notifications or workflow abuse if users believe they are only invoking a Radarr API wrapper.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill performs queueing and local state/outbox management for messaging targets, but that operational behavior is not front-and-center in the declared purpose. In practice, undeclared message-queue and tracking behavior can leak chat identifiers, create persistent records, and enable unintended notifications or workflow abuse if users believe they are only invoking a Radarr API wrapper.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill performs queueing and local state/outbox management for messaging targets, but that operational behavior is not front-and-center in the declared purpose. In practice, undeclared message-queue and tracking behavior can leak chat identifiers, create persistent records, and enable unintended notifications or workflow abuse if users believe they are only invoking a Radarr API wrapper.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill performs queueing and local state/outbox management for messaging targets, but that operational behavior is not front-and-center in the declared purpose. In practice, undeclared message-queue and tracking behavior can leak chat identifiers, create persistent records, and enable unintended notifications or workflow abuse if users believe they are only invoking a Radarr API wrapper.

Credential Access

High
Category
Privilege Escalation
Content
def _env(name: str) -> str:
    v = os.environ.get(name)
    if not v:
        raise SystemExit(f"Missing env var {name}. Set it in ~/.openclaw/.env")
    return v
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def _env(name: str) -> str:
    v = os.environ.get(name)
    if not v:
        raise SystemExit(f"Missing env var {name}. Set it in ~/.openclaw/.env")
    return v
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def _env(name: str) -> str:
    v = os.environ.get(name)
    if not v:
        raise SystemExit(f"Missing env var {name}. Set it in ~/.openclaw/.env")
    return v
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def _env(name: str) -> str:
    v = os.environ.get(name)
    if not v:
        raise SystemExit(f"Missing env var {name}. Set it in ~/.openclaw/.env")
    return v
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill declares shell, network, environment, and file read/write capabilities but does not scope or constrain them with an explicit permissions/allowed-tools declaration. In an agent setting, that increases the blast radius of misuse because the skill can fetch remote content, write local files, and invoke scripts without any machine-readable guardrails.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The trigger language is broad enough that the skill may activate on common movie-related requests without clear user intent to perform external actions. In an agent system with shell/network/file access, over-broad activation can cause unintended API calls, file writes, or media requests from casual conversation.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The chat workflow phrase 'request/add <movie>' is ambiguous and can match natural conversation in direct messages or groups, increasing the chance of accidental execution. Because this skill can enqueue tracking and trigger downstream operations, ambiguous activation is more dangerous here than in a read-only informational skill.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
PLEX_TOKEN=xxxxxxxx
```

### Optional defaults (skip prompts)
If you set these, requests can be 1‑turn because the bot won’t need to ask which profile/root folder to use.

```bash
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The guide explicitly says the bot posts progress updates and final status in the same chat, including group contexts, but does not clearly warn that movie requests and viewing interests will be visible to all chat participants. This can leak user activity, preferences, and request history to unintended audiences, especially in shared or semi-public group chats.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The manifest describes a skill for interacting with a Radarr instance via its HTTP API to search, list profiles/folders, add movies, and trigger searches. This script instead creates and writes local tracking-job JSON files under workspace/state/radarr/tracks, which is a separate local job-queue/state-management behavior not mentioned in the manifest.

External Transmission

Medium
Category
Data Exfiltration
Content
params = {"api_key": key, "query": title}
    if year:
        params["year"] = str(year)
    url = "https://api.themoviedb.org/3/search/movie?" + urllib.parse.urlencode(params)
    data = _get(url)
    results = data.get("results") or []
    if not results:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
params = {"api_key": key, "query": title}
    if year:
        params["year"] = str(year)
    url = "https://api.themoviedb.org/3/search/movie?" + urllib.parse.urlencode(params)
    data = _get(url)
    results = data.get("results") or []
    if not results:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
params = {"api_key": key, "query": title}
    if year:
        params["year"] = str(year)
    url = "https://api.themoviedb.org/3/search/movie?" + urllib.parse.urlencode(params)
    data = _get(url)
    results = data.get("results") or []
    if not results:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The script adds undocumented Plex integration to a skill whose stated purpose is Radarr-only movie management. Even though the code is not overtly malicious, this scope expansion causes the skill to access a separate media service and generate links using Plex credentials, increasing the attack surface and violating least surprise for users and reviewers. In agent environments, hidden cross-service access is dangerous because it can enable unintended data access or credential use beyond what the manifest suggests.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The code reads PLEX_URL and PLEX_TOKEN and performs authenticated Plex API requests despite the skill being described as a Radarr automation skill. This creates an undeclared credential boundary crossing: a user invoking Radarr behavior may unknowingly grant the skill access to Plex metadata and library search functions. In a multi-tool or agent setting, undisclosed use of additional secrets is a real security issue because it undermines permission transparency and can expose unrelated systems.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The script expands the skill from Radarr management into a general polling-and-notification pipeline that writes outbound message jobs to an outbox and updates persisted tracking state. That broader behavior increases the skill's authority and data-flow surface beyond the declared purpose, which can enable unintended message delivery, cross-system side effects, and hidden persistence if other components trust these outbox files.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The script conditionally consumes Plex credentials from the environment and invokes Plex link generation even though the skill is described as Radarr-focused. Pulling unrelated service credentials into the execution path broadens secret exposure and creates cross-service reach; if the helper script, workspace, or state files are compromised, Plex access may be abused or metadata exfiltrated.

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

Medium
Category
Data Flow
Content
def _run_json(cmd: list[str]) -> dict | None:
    try:
        out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True)
    except subprocess.CalledProcessError as e:
        return {"error": True, "output": e.output}
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.