Skip to content

Security: jessepesse/jellyfin-ai-recommender

SECURITY.md

Security Policy

Reporting Security Vulnerabilities

If you discover a security vulnerability, please report it by opening a private security advisory on GitHub.

Please do not report security vulnerabilities through public GitHub issues.

Known CodeQL Alerts - False Positives

SSRF (Server-Side Request Forgery) Alerts

Status: False Positive - Mitigated with defense-in-depth validation

Location: backend/src/routes/api.ts (verification endpoints), backend/src/jellyfin.ts, backend/src/authService.ts

CodeQL Alert: "The URL of this request depends on a user-provided value"

Mitigation Applied:

We have implemented 5-layer defense-in-depth SSRF protection:

  1. Entry Point Validation - All HTTP headers (x-jellyfin-url) validated with sanitizeUrl() when read
  2. Storage Validation - ConfigService.saveConfig() validates URLs before database writes
  3. Read-Time Validation - getBaseUrl() validates all URLs from config/environment
  4. Pre-Usage Validation - validateRequestUrl() validates concatenated URL strings
  5. Usage Point Validation - validateSafeUrl() wraps every axios.get/post call

Validation Functions (backend/src/utils/ssrf-protection.ts):

// All functions validate and block:
// - Cloud metadata endpoints (AWS, GCP, Azure, Alibaba)
// - Link-local addresses (169.254.0.0/16)
// - Non-HTTP protocols (file://, ftp://, etc.)

sanitizeUrl(url)        // Core validator
validateRequestUrl(url) // Pre-axios validation
validateBaseUrl(url)    // For axios.create()
validateSafeUrl(url)    // Explicit wrapper for axios calls

Example Protected Code:

// User input from HTTP header
const jellyfinServerRaw = req.headers['x-jellyfin-url'];
const jellyfinServer = jellyfinServerRaw ? sanitizeUrl(jellyfinServerRaw) : undefined; // Layer 1

// Later in the code...
const base = await getBaseUrl(jellyfinServer); // Layer 3 validation inside
const url = validateRequestUrl(`${base}/Users/${userId}/Items`); // Layer 4
const response = await axios.get(validateSafeUrl(url), { headers }); // Layer 5 - EXPLICIT

Why This Is Safe:

  1. Every URL passes through validation 5 times before reaching axios
  2. Each validation checks against blocklists and protocol restrictions
  3. Invalid URLs throw errors that prevent execution
  4. Even if one layer fails, multiple fallback layers exist

Why CodeQL Reports This:

CodeQL's static analysis tracks data flow from "user-provided value" (HTTP headers, database) to "HTTP request sink" (axios). While we validate at every step, CodeQL may not recognize our custom validateSafeUrl() function as a sanitizer without custom CodeQL query extensions.

Resolution:

This is a documented false positive. The code is secured through multiple validation layers. CodeQL alerts can be suppressed or the custom sanitizers can be registered in CodeQL configuration if needed.


Insufficient Password Hash Alert

Status: False Positive - Misidentified token signing as password hashing

Location: backend/src/middleware/auth.ts:65

CodeQL Alert: "Use of password hash with insufficient computational effort" (Rule ID: js/insufficient-password-hash)

What CodeQL Reports:

const expectedSignature = crypto.createHmac('sha256', user.passwordHash).update(payload).digest('hex');

Why This Is A False Positive:

Operation File Purpose Algorithm
Password Hashing password.ts Store user passwords ✅ PBKDF2 (1000 iterations, SHA-512)
Token Signing auth.ts:65 Sign authentication tokens ✅ HMAC-SHA256

The passwordHash variable is already a PBKDF2 hash. We use it as an HMAC signing key for tokens, NOT to hash passwords. This is a standard security pattern that:

  1. Invalidates tokens when password changes - Changing password changes the signing key
  2. Uses proper algorithms - PBKDF2 for storage, HMAC for signing
  3. Is cryptographically sound - HMAC-SHA256 is appropriate for JWT-style token signing

Actual Password Hashing Code (password.ts):

// Correct PBKDF2 implementation with salt
const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
return `${salt}:${hash}`;

Resolution:

Code includes suppression comment: lgtm[js/insufficient-password-hash]


Image Proxy SSRF Alert (#56)

Status: False Positive — mitigated with three independent validation layers

Location: backend/src/routes/system.ts (proxyRouter.get('/image', ...))

CodeQL Alert: "The URL of this request depends on a user-provided value" (Rule ID: js/request-forgery)

Why This Is A False Positive:

The imagePath query parameter passes through three independent guards before reaching axios.get:

  1. Host equality check — if the URL is absolute, its URL.host must exactly match the admin-configured Jellyseerr host extracted from ConfigService. A different host triggers layer 2.
  2. validateExternalUrl() — performs async DNS resolution on the destination hostname and rejects any address that resolves to RFC 1918 private ranges, link-local (169.254.x.x), or loopback addresses.
  3. validateSafeUrl() — final synchronous check: protocol allow-list (HTTP/HTTPS only) + blocklist of cloud metadata endpoints.

Relative paths bypass layers 1–2 because they are concatenated onto the admin-configured base URL (never user-controlled), then pass through layer 3.

CodeQL's static taint analysis cannot follow our custom sanitizer functions and therefore reports the final axios.get call as unsanitized.

Resolution:

Code includes suppression comment: lgtm[js/request-forgery]


Sensitive GET Query Parameter Alert (#57)

Status: False Positive — required for Jellyfin protocol compatibility

Location: backend/src/middleware/auth.ts (token extraction)

CodeQL Alert: "Route handler for GET requests uses query parameter as sensitive data" (Rule ID: js/sensitive-get-query)

Why This Is A False Positive:

The Jellyfin media server protocol requires clients to pass authentication tokens via ?api_key= query parameter for certain request types (image requests, media stream URLs) where HTTP headers cannot be set by the client. This is a documented part of the Jellyfin API contract — removing it would break Jellyfin client compatibility.

Mitigations applied to reduce the risk of token exposure:

  • The raw token is never written to application logs — only a SHA-256 HMAC of the token (keyed with a per-process secret) is stored in the token cache.
  • The token cache has a 5-minute TTL (NodeCache stdTTL: 300).
  • Prefer headers (X-Access-Token, X-Emby-Token) over query params — the query param is the last fallback.

Resolution:

Code includes suppression comment: lgtm[js/sensitive-get-query]

Clear Text Storage of Sensitive Information (#58)

Status: ✅ Fixed in v2.7.0 — Backend session management implemented

Previous location: frontend/src/contexts/AuthContext.tsx (sessionStorage password) — removed

CodeQL Alert: "Sensitive data is stored in clear text" (Rule ID: js/clear-text-storage-of-sensitive-data)

Resolution:

The password is no longer stored in the browser at all. Backend session management was implemented in v2.7.0:

  • Login creates a Session record in the database
  • Jellyfin token and credential stored AES-256-GCM encrypted in the database
  • Frontend stores only an opaque 64-hex session token (never the password)
  • Backend re-authenticates transparently when Jellyfin token expires using the encrypted credential
  • Session tokens invalidated server-side on logout

Key files: backend/src/services/sessionService.ts, backend/src/utils/session.ts, backend/src/utils/secretManager.ts

Security Measures Implemented

Input Validation

  • ✅ Zod schemas for all API inputs
  • ✅ Express-validator middleware
  • ✅ Type-safe request handling

SSRF Protection

  • ✅ 5-layer defense-in-depth URL validation
  • ✅ Blocklist for cloud metadata endpoints
  • ✅ Protocol restrictions (HTTP/HTTPS only)
  • ✅ Link-local address blocking

Rate Limiting

  • ✅ Authentication: 5 attempts per 15 minutes
  • ✅ Recommendations: 10 requests per 5 minutes
  • ✅ General API: 100 requests per 15 minutes

Security Headers (Helmet)

  • ✅ XSS protection
  • ✅ Clickjacking protection (X-Frame-Options)
  • ✅ MIME sniffing protection
  • ✅ Content Security Policy

Additional Protections

  • ✅ ReDoS prevention (safe regex patterns)
  • ✅ Format string injection prevention
  • ✅ No sensitive data logging
  • ✅ CORS configuration
  • ✅ Session token stored in localStorage (never password or raw credentials)

Supported Versions

Version Supported
2.8.x
2.7.x
2.6.x
< 2.6

Security Update Policy

Security patches are released as soon as possible after discovery and verification. Users are encouraged to update to the latest version.

There aren't any published security advisories