Back to skill

Security audit

Overseerr

Security checks for vulnerabilities and agentic risk

Overview

This skill is a straightforward Overseerr helper, but users should configure it carefully because it can create media requests and sends an API key to the configured server.

Install only if you intend the agent to create and inspect Overseerr requests. Use an HTTPS Overseerr URL for LAN or remote servers, keep the API key narrowly scoped where possible, and prefer searching first or using exact titles when there may be multiple matches.

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

Warning
Location
scripts/lib.mjs:7
Finding
Overseerr API credentials can be transmitted over plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lib.mjs:7-10, 45-85` and `SKILL.md:16-19` **Vulnerability Type**: Sensitive credential transmission over an unencrypted channel **Risk Level**: Medium ### Vulnerable Code The configuration accepts the supplied base URL without validating its protocol or restricting plaintext HTTP to loopback addresses: ```javascript export function getConfig() { const baseUrl = requiredEnv('OVERSEERR_URL').replace(/\/$/, ''); const apiKey = requiredEnv('OVERSEERR_API_KEY'); return { baseUrl, apiKey }; } ``` The API key is then attached to network requests through the `X-Api-Key` header: ```javascript export async function overseerrFetch(path, { method = 'GET', query, body } = {}) { const { baseUrl, apiKey } = getConfig(); const url = new URL(`${baseUrl}/api/v1${path}`); if (query) { for (const [key, value] of Object.entries(query)) { if (value === undefined || value === null) continue; url.searchParams.set(key, String(value)); } // Overseerr's backend validation expects strict URL encoding; URLSearchParams encodes spaces as '+', // which the API rejects. Normalize '+' to '%20'. url.search = url.search.replace(/\+/g, '%20'); } const headers = { 'X-Api-Key': apiKey, Accept: 'application/json', }; const isMutation = method !== 'GET' && method !== 'HEAD'; if (isMutation) { const csrf = await getCsrfContext({ baseUrl, apiKey }); if (csrf.enabled) { if (csrf.cookieHeader) headers.Cookie = csrf.cookieHeader; if (csrf.xsrfToken) { headers['X-CSRF-Token'] = csrf.xsrfToken; headers['X-XSRF-TOKEN'] = csrf.xsrfToken; } } } let payload; if (body !== undefined) { headers['Content-Type'] = 'application/json'; payload = JSON.stringify(body); } const res = await fetch(url, { method, headers, body: payload, }); ``` The documentation explicitly provides a plaintext HTTP example: ```mar ...[truncated 2223 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse and validate `OVERSEERR_URL` before returning it from `getConfig()`. 2. Require the `https:` protocol for all non-loopback destinations. 3. If local plaintext operation must remain supported, permit `http:` only for explicit loopback hosts such as `localhost`, `127.0.0.1`, and `[::1]`. 4. Reject unsupported protocols and URLs containing embedded username or password credentials. 5. Update `SKILL.md` to state that HTTPS is mandatory for LAN and remote Overseerr instances. 6. Consider rejecting automatic cross-origin redirects, or validate every redirect destination before forwarding the API key, cookies, or CSRF tokens. 7. Use a narrowly privileged Overseerr API credential where the server supports credential scoping. 8. Warn operators that disabling certificate verification or using untrusted certificates would undermine the transport protection. Example validation approach: ```javascript function validateBaseUrl(value) { const url = new URL(value); const loopbackHosts = new Set(['localhost', '127.0.0.1', '[::1]']); const isLoopback = loopbackHosts.has(url.hostname); if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopback)) { throw new Error( 'OVERSEERR_URL must use HTTPS unless it points to an explicit loopback host' ); } if (url.username || url.password) { throw new Error('OVERSEERR_URL must not contain embedded credentials'); } return url.toString().replace(/\/$/, ''); } export function getConfig() { const baseUrl = validateBaseUrl(requiredEnv('OVERSEERR_URL')); const apiKey = requiredEnv('OVERSEERR_API_KEY'); return { baseUrl, apiKey }; } ``` ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
Findings (1)

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This script performs a state-changing POST to Overseerr's /request endpoint immediately after selecting the first search result, with no confirmation prompt, dry-run mode, or inline warning. In an agent/tooling context, this can cause unintended media requests from ambiguous titles, prompt injection, or accidental invocation, leading to unauthorized or unwanted changes in the connected media management system.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/lib.mjs:2