A ZIP file with a poisoned path lets a course editor replace your web app's brain with a backdoor
CVE-2026-39405 is a path-traversal flaw in Frappe LMS versions ≤ 2.50.0 (some sources say ≤ 2.49.0). The SCORM upload handler extracts ZIP archives using the chapter title as part of the destination path *without sanitizing directory traversal sequences*. An authenticated user who holds the course editor role can set a chapter title to something like ../../../../../apps/lms/lms/lms and include a malicious api.py in the ZIP. Extraction writes the file straight into the application root, replacing the legitimate API module with a backdoor. From there, one HTTP request to the overwritten endpoint gives the attacker full remote code execution as the web worker process. Rhino Security Labs documented a chained attack (CVE-2026-34606 stored XSS → CVE-2026-39405 path traversal) that escalates from a *student* account all the way to RCE.
Third-party tools assign a CVSS 4.0 base of 9.4 (Critical) using AV:N/AC:L/AT:N/PR:L/UI:N, but no official vendor or NVD CVSS 3.1 score exists. That 9.4 overstates real-world risk for most defenders. The vector marks *Privileges Required: Low*, yet the actual prerequisite is course editor, a role typically granted only to a handful of trusted administrators or content authors — not every authenticated user. The installed base of Frappe LMS is small (niche open-source project, not enterprise SaaS), and the SCORM feature itself is optional. With EPSS at 0.45% and no KEV listing, the probability of opportunistic exploitation at scale is low. The vuln is real and dangerous *if you run Frappe LMS*, but equating it to a 9.4 across a fleet of 10,000 hosts misrepresents the actual exposure.
5 steps from start to impact.
Obtain course editor credentials
- Valid Frappe LMS account with course editor role OR ability to chain CVE-2026-34606 XSS
- Network access to the Frappe LMS web interface
- Course editor is a privileged role granted to a small subset of users
- XSS chain requires an admin to visit the attacker's profile page
Craft malicious SCORM ZIP archive
api.py) at a path that includes directory traversal sequences. The chapter title field is set to a traversal payload like ../../../../../apps/lms/lms/lms. When combined with the ZIP entry's internal path, this resolves to the application's source directory on disk.- Knowledge of Frappe's on-disk layout (well-documented, open source)
- Ability to create ZIP files with crafted paths
- Requires knowledge of the target's bench directory structure, though defaults are well-known
Upload SCORM package via Add Chapter dialog
public/scorm directory.- Active course editor session
- SCORM upload feature enabled (default in affected versions)
- If the Frappe instance doesn't use SCORM features, the upload endpoint may still exist but may be less obvious to the attacker
../ sequences in multipart form fields or JSON payloads. File upload logging in Frappe's request log.ZIP extraction overwrites application source files
api.py overwrites the legitimate module in the application source tree. The web worker process will load this file on the next request to the affected endpoint, or immediately if the module is re-imported.- Write permissions on the application directory (typical for the bench user running the web worker)
- File integrity monitoring (FIM) tools like AIDE or OSSEC would alert on changes to application source files
apps/lms/lms/lms/api.py or similar application files. Host-based IDS signatures for unexpected file writes outside web upload directories.Execute arbitrary commands via backdoor endpoint
frappe or www-data). From here, the attacker can read the database credentials from site_config.json, dump user data, pivot to other services on the host, or attempt privilege escalation.- Network access to the Frappe web interface
- EDR on the host would flag subprocess spawning from a Python web worker
- Network segmentation limits lateral movement from the LMS host
python process spawning /bin/sh or /bin/bash. Network IDS signatures for reverse shell traffic. Anomalous outbound connections from the LMS host.../ sequences in file upload and chapter title fields. This catches the traversal payload before it reaches the application. Deploy within 30 days.apps/lms/ directory tree. Alert on any modification to .py files outside of a controlled deployment window. This does not prevent the write but detects it within minutes, enabling rapid response.public/scorm. Apply within 180 days per the noisgate remediation SLA for HIGH severity. If you can patch sooner, do so — the upgrade is low-risk.- Disabling SCORM at the feature-flag level — Frappe LMS does not have a clean feature toggle for SCORM uploads. The upload endpoint exists regardless of whether you use SCORM content. You would need to remove the route at the web server level (nginx/Apache) to truly block it.
- Content Security Policy (CSP) headers — CSP mitigates the XSS chain (CVE-2026-34606) but does nothing to prevent a legitimate course editor from uploading a malicious ZIP directly. It addresses the escalation path, not the core vulnerability.
- Rate limiting on uploads — The attack requires only a single upload. Rate limiting does not meaningfully reduce risk.
The supporting signals.
| In-the-Wild Exploitation | No known exploitation in the wild. Not listed on CISA KEV. No GreyNoise or Shadowserver tags observed for this CVE. The niche install base makes mass exploitation unlikely. |
|---|---|
| Proof of Concept | Full exploit chain documented by Rhino Security Labs in their blog post Multiple Vulnerabilities in Frappe LMS Leading to Remote Code Execution. The writeup details the SCORM ZIP crafting, path traversal payload, and backdoor placement. Reported by @nickhefty. No standalone weaponized exploit tool found on GitHub or pocindex.io, but the blog provides step-by-step reproduction. |
| EPSS Score | 0.00454 (0.45%) — 22nd–23rd percentile. Low predicted exploitation probability within the next 30 days. |
| KEV Status | Not listed. No CISA KEV entry as of 2026-09-27. |
| CVSS Vector | CVSS 4.0: AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H → 9.4 Critical (third-party scoring; no official NVD or vendor CVSS 3.1 score exists). PR:L underrepresents the real prerequisite: course editor role, not just any authenticated user. |
| Affected Versions | Frappe LMS ≤ 2.50.0 (GHSA says ≤ 2.49.0). All prior versions with SCORM support are affected. |
| Fixed Version | 2.50.1 — adds path validation to ensure ZIP extraction stays within the public/scorm directory. No distro backports; Frappe LMS is installed via bench or pip. |
| Exposure / Install Base | Frappe LMS is a niche open-source Python LMS built on the Frappe Framework. No Shodan/Censys/FOFA fingerprint data available. Typically self-hosted by organizations already running ERPNext. The GitHub repo has moderate stars but no public deployment telemetry. Expect a very small internet-facing footprint. |
| Disclosure Date | 2026-05-20 — coordinated disclosure via GitHub Security Advisory GHSA-mxh7-g3r7-g96h. |
| Reporting Researcher | @nickhefty (credited in GHSA). Full chain research published by Rhino Security Labs, who also documented the XSS-to-RCE chain via CVE-2026-34606. |
Sources.
- GitHub Security Advisory GHSA-mxh7-g3r7-g96h
- Rhino Security Labs — Multiple Vulnerabilities in Frappe LMS Leading to RCE
- Endor Labs — CVE-2026-39405 Vulnerability Detail
- Strix.ai — CVE-2026-39405 Path Traversal (CVSS 9.4)
- OpenCVE — CVE-2026-39405 Details
- Exploit Intelligence Platform — CVE-2026-39405
- OSV — CVE-2026-39405
Why this verdict
- Authentication + role gate narrows the attacker pool: The chain requires course editor privileges, not just any authenticated session. In typical Frappe LMS deployments, this role is held by 1–5 users out of potentially hundreds. This is not PR:L in the CVSS sense — it is a specific privileged role, which significantly reduces the reachable attacker population.
- Tiny install base limits fleet exposure: Frappe LMS is a niche open-source project. For a defender managing 10,000 hosts, the probability of even *one* host running Frappe LMS is low. There is no Shodan/Censys fingerprint data suggesting meaningful internet-facing exposure. This is not a commodity stack like Apache or nginx.
- Full RCE on successful exploitation keeps the floor at HIGH: Despite the friction, a successful attack yields arbitrary code execution as the web worker. The attacker can read database credentials, dump PII (student records, training data), and pivot laterally. This is a real RCE, not a theoretical one — Rhino Security Labs demonstrated end-to-end exploitation.
- XSS chain (CVE-2026-34606) lowers the effective privilege requirement: The documented attack path chains stored XSS to hijack an admin session, meaning a *student-level* attacker can reach the course editor upload. This partially offsets the role-gate friction, though it still requires social engineering (admin must visit attacker's profile).
- Role multiplier: (a) *Low-value role (dev/sandbox):* chain succeeds, blast radius is host-local, negligible. (b) *Typical role (internal training LMS):* chain succeeds, blast radius is host + database (student PII, training records). (c) *High-value co-deployment:* if Frappe LMS is co-hosted with ERPNext on the same bench, RCE on the LMS host compromises the ERP database (financials, HR, payroll). This is a plausible deployment pattern for Frappe shops. However, Frappe LMS is NOT a canonically high-value-role component (not an IdP, DC, hypervisor, PKI, CI/CD, backup, or network edge). The worst plausible outcome is host+database compromise, not domain/fleet/supply-chain takeover. Floor is HIGH, not CRITICAL.
- No in-the-wild exploitation and low EPSS (0.45%) confirm no urgency escalation is needed. The vuln has been public for 4+ months with no KEV listing and no observed scanning campaigns.
Why not higher?
A CRITICAL rating would be appropriate if Frappe LMS were a widely deployed, canonically high-value component (like a hypervisor, domain controller, or network edge appliance) where exploitation yields fleet-scale or identity-scale compromise. It is none of those things. The attack requires authenticated access with a specific privileged role, the install base is tiny, and there is no evidence of in-the-wild exploitation. The XSS chain adds reachability but also adds steps and social engineering friction.
Why not lower?
A MEDIUM rating would understate the impact. This is a proven, fully documented RCE chain with a public writeup from a reputable security lab. The path from course editor to code execution is short and reliable (one ZIP upload). The XSS chain means even student-level users can reach it. If you run Frappe LMS, especially co-hosted with ERPNext, the blast radius on successful exploitation is significant: full database access, lateral movement, and potential PII exposure.
Crowdsourced verification payload.
Run this script on the Frappe LMS host as the bench user or root. Pass the bench directory as an argument (defaults to /home/frappe/frappe-bench). No network access required. Usage: bash check_cve_2026_39405.sh /home/frappe/frappe-bench
#!/usr/bin/env bash
# CVE-2026-39405 - Frappe LMS SCORM Path Traversal Version Check
# Outputs: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 1=VULNERABLE, 0=PATCHED, 2=UNKNOWN
set -euo pipefail
BENCH_DIR="${1:-/home/frappe/frappe-bench}"
APP_DIR="$BENCH_DIR/apps/lms"
if [ ! -d "$APP_DIR" ]; then
echo "UNKNOWN - Frappe LMS app directory not found at $APP_DIR"
exit 2
fi
VERSION=""
# Try __init__.py
if [ -f "$APP_DIR/lms/__init__.py" ]; then
VERSION=$(grep -oP '__version__\s*=\s*["'\'']+\K[^"'\'']+' "$APP_DIR/lms/__init__.py" 2>/dev/null || true)
fi
# Try setup.cfg
if [ -z "$VERSION" ] && [ -f "$APP_DIR/setup.cfg" ]; then
VERSION=$(grep -oP '^version\s*=\s*\K.*' "$APP_DIR/setup.cfg" 2>/dev/null || true)
fi
# Try git tag
if [ -z "$VERSION" ]; then
VERSION=$(cd "$APP_DIR" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || true)
fi
if [ -z "$VERSION" ]; then
echo "UNKNOWN - Could not determine Frappe LMS version in $APP_DIR"
exit 2
fi
echo "Detected Frappe LMS version: $VERSION"
# Parse major.minor.patch
IFS='.' read -r MAJ MIN PATCH <<< "$VERSION"
PATCH="${PATCH:-0}"
# Fixed in 2.50.1
if [ "$MAJ" -lt 2 ] 2>/dev/null; then
echo "VULNERABLE - Frappe LMS $VERSION < 2.50.1 (CVE-2026-39405)"
exit 1
elif [ "$MAJ" -eq 2 ] && [ "$MIN" -lt 50 ] 2>/dev/null; then
echo "VULNERABLE - Frappe LMS $VERSION < 2.50.1 (CVE-2026-39405)"
exit 1
elif [ "$MAJ" -eq 2 ] && [ "$MIN" -eq 50 ] && [ "$PATCH" -lt 1 ] 2>/dev/null; then
echo "VULNERABLE - Frappe LMS $VERSION < 2.50.1 (CVE-2026-39405)"
exit 1
else
echo "PATCHED - Frappe LMS $VERSION >= 2.50.1 (CVE-2026-39405 fixed)"
exit 0
fi