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 }; } ``` ]]>
