Back to skill

Security audit

Jellyfin Control

Security checks for vulnerabilities and agentic risk

Overview

This skill mostly does what it says, but its Android TV ADB path can turn user-controlled input into host shell commands and its examples send sensitive tokens over plaintext HTTP.

Review before installing. Use HTTPS or a trusted encrypted tunnel for Jellyfin and Home Assistant, use least-privileged dedicated API keys/tokens, avoid admin Jellyfin credentials unless you need history or scan, and do not use the direct ADB backend until ADB_DEVICE and app IDs are validated or ADB execution is changed to argument-based process calls.

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
tv.js:209
Finding
OS Command Injection Through ADB Device and Application Parameters<![CDATA[ ## Vulnerability Details **File Location**: `tv.js:209-216`, `tv.js:242`, `tv.js:294-300`; attacker-controlled application input originates from `cli.js:137` **Vulnerability Type**: OS command injection **Risk Level**: High ### Vulnerable Code ```js function adbExec(command, throwOnError = true) { const device = TV_CONFIG.adbDevice; const prefix = device ? `adb -s ${device}` : 'adb'; const fullCmd = `${prefix} ${command}`; try { return execSync(fullCmd, { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }).trim(); } catch (e) { if (throwOnError) { throw new Error(`ADB failed: ${fullCmd}\n${e.stderr || e.message}`); } return ''; } } ``` The configured device is also inserted into an ADB subcommand: ```js function adbConnect() { const device = TV_CONFIG.adbDevice; if (!device) { throw new Error('ADB_DEVICE not configured. Set it to "TV_IP:5555" (e.g. "192.168.1.100:5555").'); } const devices = adbExec('devices', false); if (devices.includes(device) && !devices.includes('offline')) return; const result = adbExec(`connect ${device}`); ``` The application ID supplied through the CLI is inserted into the shell command: ```js async launchApp(appId) { adbCheckInstalled(); adbConnect(); // monkey is the most reliable universal launcher const result = adbExec(`shell monkey -p ${appId} -c android.intent.category.LAUNCHER 1`, false); if (result.includes('No activities found')) { // Fallback: leanback launcher intent (Android TV specific) adbExec( `shell am start -a android.intent.action.MAIN -c android.intent.category.LEANBACK_LAUNCHER ${appId}`, false ); } console.log(`✅ Launched app: ${appId}`); }, ``` The CLI passes a user-controlled value to this operation: ```js case 'launch': await tv. ...[truncated 2892 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace shell-based execution with argument-based process invocation. For example, use `execFileSync` or `spawnSync`: ```js const { execFileSync } = require('child_process'); function adbExec(args, throwOnError = true) { const device = TV_CONFIG.adbDevice; const adbArgs = device ? ['-s', device, ...args] : args; try { return execFileSync('adb', adbArgs, { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }).trim(); } catch (e) { if (throwOnError) { throw new Error(`ADB failed: ${e.stderr || e.message}`); } return ''; } } ``` 2. Pass every ADB token as a separate array element, such as: ```js adbExec(['connect', device]); adbExec([ 'shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1' ]); ``` 3. Validate `ADB_DEVICE` against a strict expected format. Permit only a valid IPv4/IPv6 address or approved hostname and a numeric port. Reject whitespace and all shell metacharacters. 4. Validate application package names with a restrictive allowlist, for example: ```js if (!/^[A-Za-z0-9._-]+$/.test(appId)) { throw new Error('Invalid application ID'); } ``` 5. Prefer a configured allowlist of launchable application IDs instead of accepting arbitrary package names from agent or CLI input. 6. Do not send concatenated free-form ADB commands through Home Assistant. If the integration cannot accept structured arguments, strictly validate `appId` before constructing the command and limit the Home Assistant token and Android TV integration to the least privileges possible. 7. Add automated tests that verify shell metacharacters, whitespace, substitutions, redirections, and newline characters are rejected and cannot create side effects. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
jellyfin.js:23
Finding
Sensitive Jellyfin and Home Assistant Credentials Permitted Over Plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `jellyfin.js:23-31`, `tv.js:104-122`; insecure examples are documented in `SKILL.md:35-36`, `SKILL.md:53-57`, and `README.md:54-58` **Vulnerability Type**: Cleartext transmission of sensitive credentials **Risk Level**: Medium ### Vulnerable Code The Jellyfin client sends its API token through whichever protocol is specified by `JF_URL`: ```js const api = axios.create({ baseURL: CONFIG.url, headers: { 'X-Emby-Token': CONFIG.apiKey, // Default to API Key 'X-Emby-Authorization': `MediaBrowser Client="${CONFIG.deviceName}", Device="${CONFIG.deviceName}", DeviceId="${CONFIG.deviceId}", Version="${CONFIG.clientVersion}"`, 'Content-Type': 'application/json' } }); ``` The Home Assistant client explicitly supports both HTTP and HTTPS while sending a bearer token: ```js function haRequest(service, domain, data) { return new Promise((resolve, reject) => { const url = new URL(`/api/services/${domain}/${service}`, TV_CONFIG.haUrl); const isHttps = url.protocol === 'https:'; const lib = isHttps ? https : http; const body = JSON.stringify(data); const options = { method: 'POST', hostname: url.hostname, port: url.port || (isHttps ? 443 : 80), path: url.pathname, headers: { 'Authorization': `Bearer ${TV_CONFIG.haToken}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }; ``` The documentation recommends plaintext URLs for both sensitive services: ```json "JF_URL": "http://192.168.1.50:8096", "JF_API_KEY": "your-jellyfin-api-key", "JF_USER": "victor", "HA_URL": "http://192.168.1.138:8123", "HA_TOKEN": "your-ha-long-lived-token" ``` ### Technical Analysis The application does not require TLS for Jellyfin or Home Assistant connections. When an `http://` URL is configured, Jellyfin ...[truncated 2376 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require `https:` for all non-loopback Jellyfin and Home Assistant URLs by default. 2. Reject insecure protocols during configuration validation: ```js function requireSecureServiceUrl(value, name) { const url = new URL(value); const loopback = ['localhost', '127.0.0.1', '::1'].includes(url.hostname); if (url.protocol !== 'https:' && !loopback) { throw new Error(`${name} must use HTTPS for non-loopback connections`); } return url.toString(); } ``` 3. If legacy HTTP support is necessary, require an explicit option such as `ALLOW_INSECURE_HTTP=true`, emit a prominent warning, and document the resulting credential-exposure risk. 4. Update all examples in `README.md` and `SKILL.md` to use HTTPS URLs. 5. Recommend a trusted TLS reverse proxy for Jellyfin and Home Assistant deployments that do not provide HTTPS directly. 6. Retain normal certificate verification. Do not address certificate errors by globally disabling TLS validation. 7. Use dedicated, least-privileged Jellyfin and Home Assistant credentials so that compromise does not grant unnecessary administrative access. 8. Where TLS deployment is impractical, use a trusted encrypted tunnel or private overlay network rather than transmitting credentials over ordinary LAN HTTP. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (29)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The description emphasizes playback and TV control, but the document also advertises history retrieval, library statistics, and library scan operations. Undeclared or under-declared administrative behaviors are dangerous because users may authorize the skill for simple media control without realizing it can access activity data or trigger state-changing server operations.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The description emphasizes playback and TV control, but the document also advertises history retrieval, library statistics, and library scan operations. Undeclared or under-declared administrative behaviors are dangerous because users may authorize the skill for simple media control without realizing it can access activity data or trigger state-changing server operations.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The description emphasizes playback and TV control, but the document also advertises history retrieval, library statistics, and library scan operations. Undeclared or under-declared administrative behaviors are dangerous because users may authorize the skill for simple media control without realizing it can access activity data or trigger state-changing server operations.

Credential Access

High
Category
Privilege Escalation
Content
| `TV_BACKEND`       | All       | Force backend: `homeassistant`, `webos`, `androidtv`, or `auto`       |
| `TV_PLATFORM`      | HA        | Force platform: `webos` or `androidtv` (auto-detected from entity)    |
| `HA_URL`           | HA        | Home Assistant URL, e.g. `http://192.168.1.138:8123`                  |
| `HA_TOKEN`         | HA        | HA long-lived access token (Profile → Long-Lived Access Tokens)       |
| `HA_TV_ENTITY`     | HA        | Entity ID of your TV, e.g. `media_player.lg_webos_tv_oled48c34la`     |
| `TV_IP`            | WebOS     | LG TV IP address for direct WebOS SSAP connection                     |
| `TV_CLIENT_KEY`    | WebOS     | Pairing key (printed on first connection — save it!)                  |
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
| `TV_BACKEND`       | All       | Force backend: `homeassistant`, `webos`, `androidtv`, or `auto`       |
| `TV_PLATFORM`      | HA        | Force platform: `webos` or `androidtv` (auto-detected from entity)    |
| `HA_URL`           | HA        | Home Assistant URL, e.g. `http://192.168.1.138:8123`                  |
| `HA_TOKEN`         | HA        | HA long-lived access token (Profile → Long-Lived Access Tokens)       |
| `HA_TV_ENTITY`     | HA        | Entity ID of your TV, e.g. `media_player.lg_webos_tv_oled48c34la`     |
| `TV_IP`            | WebOS     | LG TV IP address for direct WebOS SSAP connection                     |
| `TV_CLIENT_KEY`    | WebOS     | Pairing key (printed on first connection — save it!)                  |
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Ae1

High
Category
analysis-evasion
Content
- `cli.js` — User-friendly CLI with all commands
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
npm install ws

# If using direct ADB backend (Android TV / Fire TV):
sudo apt install adb    # Debian/Ubuntu
# or: brew install android-platform-tools   # macOS
```
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
npm install ws

# If using direct ADB backend (Android TV / Fire TV):
sudo apt install adb    # Debian/Ubuntu
# or: brew install android-platform-tools   # macOS
```
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill declares required environment variables, including sensitive API keys and tokens, but does not declare an explicit tool scope or permission boundary. That omission makes the skill's access expectations less transparent and weakens reviewability, increasing the chance that a caller grants broader capabilities than intended.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The skill metadata describes playback/session/TV control, but the CLI also exposes a library administration action via `scan`. Hidden or undocumented administrative capabilities expand the effective permission surface and can surprise users or orchestrators into invoking a more impactful action than expected. In this context, triggering a library scan is not destructive in the classic sense, but it can consume server resources and perform an administrative operation outside the declared scope.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The skill exposes admin-only capabilities to read activity history and trigger a library refresh, which exceed the stated playback/control scope. In an agent context, unnecessary privileged actions increase attack surface and can enable privacy-invasive access to viewing history or unauthorized administrative operations if the skill is invoked with elevated Jellyfin credentials.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
This file includes server-administration behavior not justified by a media playback controller, namely reading system activity logs and initiating library scans. Even though Jellyfin enforces privileges server-side, embedding these actions in a broadly usable playback skill creates scope creep and raises the risk of misuse when an admin API key or admin password is configured.

Context-Inappropriate Capability

Medium
Confidence
81% confidence
Finding
The manifest describes media-server and TV control, but does not indicate that the skill will execute local system binaries via child_process. Running adb through execSync gives the skill host-level command execution dependencies that are not apparent from the stated purpose.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
This method invokes Home Assistant's `media_player.turn_off`, which can immediately power off the user's TV. The code emits only a success log after execution and does not present any warning or confirmation before performing the irreversible/disruptive action.

Description-Behavior Mismatch

Medium
Confidence
87% confidence
Finding
The manifest describes controlling Jellyfin playback flow and TV power/apps, but this file also implements listing installed applications/packages on the TV through Home Assistant. Enumerating installed apps is a broader device-inspection capability than the stated one-command Jellyfin control workflow and is not mentioned in the manifest description.

Description-Behavior Mismatch

Medium
Confidence
83% confidence
Finding
The manifest frames the skill as controlling Jellyfin media server and TV, but the implementation provides a general-purpose ADB command executor used for package listing, connection management, key events, and app launching. This introduces a broader device-control surface than the manifest explicitly claims, even though some uses support TV control.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
throw new Error(
            'adb is not installed or not in PATH.\n' +
            'Install it:\n' +
            '  • Debian/Ubuntu: sudo apt install adb\n' +
            '  • macOS:         brew install android-platform-tools\n' +
            '  • Or use TV_BACKEND=homeassistant instead'
        );
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The Android TV `turnOff` method uses ADB to send `KEYCODE_SLEEP`, changing the state of the target device in a potentially disruptive way. Although the code logs success afterward, it does not provide any prior user disclosure or confirmation that the operation will immediately put the TV to sleep.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The `turnOff` method for the WebOS backend sends `system/turnOff`, which is a destructive/disruptive operation affecting device state. Unlike pairing or wake operations, there is no user-facing warning, confirmation prompt, or cautionary comment around this shutdown behavior in the code shown.

Missing User Warnings

Low
Confidence
85% confidence
Finding
This markdown file describes commands that power on/off a TV, launch apps, and send playback/volume controls, which can affect system state and connected devices. While the usage is explicit, there is no direct warning in the usage section that these commands will immediately control the user's TV and alter playback/device state.

Description-Behavior Mismatch

Low
Confidence
90% confidence
Finding
The manifest does not disclose that the skill can inspect user activity history, yet the CLI provides a `history` command that reveals viewing activity. This is a scope/consent issue: even if the underlying API legitimately supports it, exposing user-behavior data without clear declaration can lead to privacy surprises and misuse in multi-user environments.

Description-Behavior Mismatch

Low
Confidence
91% confidence
Finding
The CLI exposes a `stats` command for library analytics, but this capability is not mentioned in the manifest. While lower risk than direct control or admin actions, it still expands data exposure beyond the documented behavior and may leak information about the media library's size and composition.

Missing User Warnings

Low
Confidence
80% confidence
Finding
The `scan` command immediately triggers a library refresh without warning, confirmation, or friction. Because library scans can be resource-intensive and may affect server responsiveness, executing them as a one-step command increases the chance of accidental or repeated administrative impact, especially when invoked through automation or an agent.

Missing User Warnings

Low
Confidence
78% confidence
Finding
This code reads a password from the JF_PASS environment variable and sends it to /Users/AuthenticateByName, but the only user-facing log messages about login are commented out. For code files, credential use and network transmission of sensitive data should have some visible disclosure unless clearly surfaced elsewhere; in this file, that disclosure is absent in the active execution path.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"author": "Titunito",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.13.5",
    "fuse.js": "^7.1.0",
    "yargs": "^18.0.0"
  },
Confidence
93% confidence
Finding
The axios dependency is specified with a caret range, which allows newer minor/patch versions to be installed over time. This reduces build reproducibility and can expose the skill to accidental supply-chain risk or unexpected vulnerable releases, though by itself it is a hardening issue rather than an immediate exploitable flaw in this manifest.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.env_credential_access

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
tv.js:213

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
jellyfin.js:7