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.
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:
- Entry Point Validation - All HTTP headers (
x-jellyfin-url) validated withsanitizeUrl()when read - Storage Validation -
ConfigService.saveConfig()validates URLs before database writes - Read-Time Validation -
getBaseUrl()validates all URLs from config/environment - Pre-Usage Validation -
validateRequestUrl()validates concatenated URL strings - Usage Point Validation -
validateSafeUrl()wraps everyaxios.get/postcall
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 callsExample 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 - EXPLICITWhy This Is Safe:
- Every URL passes through validation 5 times before reaching axios
- Each validation checks against blocklists and protocol restrictions
- Invalid URLs throw errors that prevent execution
- 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.
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:
- Invalidates tokens when password changes - Changing password changes the signing key
- Uses proper algorithms - PBKDF2 for storage, HMAC for signing
- 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]
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:
- Host equality check — if the URL is absolute, its
URL.hostmust exactly match the admin-configured Jellyseerr host extracted fromConfigService. A different host triggers layer 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.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]
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]
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
Sessionrecord 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
- ✅ Zod schemas for all API inputs
- ✅ Express-validator middleware
- ✅ Type-safe request handling
- ✅ 5-layer defense-in-depth URL validation
- ✅ Blocklist for cloud metadata endpoints
- ✅ Protocol restrictions (HTTP/HTTPS only)
- ✅ Link-local address blocking
- ✅ Authentication: 5 attempts per 15 minutes
- ✅ Recommendations: 10 requests per 5 minutes
- ✅ General API: 100 requests per 15 minutes
- ✅ XSS protection
- ✅ Clickjacking protection (X-Frame-Options)
- ✅ MIME sniffing protection
- ✅ Content Security Policy
- ✅ 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)
| Version | Supported |
|---|---|
| 2.8.x | ✅ |
| 2.7.x | ✅ |
| 2.6.x | ❌ |
| < 2.6 | ❌ |
Security patches are released as soon as possible after discovery and verification. Users are encouraged to update to the latest version.