← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-39405 · CWE-22 · Disclosed 2026-05-20

Frappe Learning Management System

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is a smuggled crate that only matters if you already have a badge to the loading dock

CVE-2026-39405 is a ZIP-slip style path traversal in Frappe LMS SCORM package handling. In Frappe LMS 2.50.0 and below, a user who already has the course editing role can upload a crafted SCORM ZIP whose file names contain traversal sequences, causing extraction outside the intended public/scorm/... directory; the fix landed in 2.50.1.

In a vacuum, arbitrary file write by a web app is scary enough to tempt a CRITICAL label. In real enterprise conditions, the biggest truth is the friction: this is not unauthenticated internet RCE; it requires a valid account, the right role, and use of a specific LMS feature. That pushes it down to = ASSESSED AT HIGH even though the downstream impact can still become code execution or full site compromise on badly isolated deployments.

"ASSESSED AT HIGH: nasty arbitrary file write, but it starts with a course-editor account and a niche SCORM workflow"
02 · The Attack Path

4 steps from start to impact.

STEP 01

Get a course-editor foothold

The attacker first needs authenticated access to Frappe LMS with privileges to edit courses and upload SCORM content. In practice that means insider misuse, account takeover, SSO compromise, or reuse of an over-privileged training account. Common tooling here is standard phishing or credential-stuffing infrastructure rather than a CVE-specific exploit.
Conditions required:
  • Reachability to the Frappe LMS instance
  • A valid user account
  • That account has the course editing role
Where this breaks in practice:
  • Requires prior access or insider status
  • MFA and IdP controls can stop the account-takeover path
  • Many LMS deployments keep course-authoring privileges limited to a small subset of users
Detection/coverage: Identity logs and SSO telemetry usually cover this step well; external vulnerability scanners do not.
STEP 02

Build a traversal SCORM package

Using zip, Python zipfile, or Burp-assisted content generation, the attacker crafts a SCORM archive containing file names like ../../../target. The patch in PR #2274 shows the root bug directly: the original code called extractall() without validating each archive member path.
Conditions required:
  • Ability to create or modify a ZIP archive
  • Knowledge of likely writable target paths
Where this breaks in practice:
  • Attacker still needs a destination that is both writable by the app user and useful for follow-on impact
  • Blind arbitrary write is much less useful without filesystem/path knowledge
Detection/coverage: Very little pre-execution coverage unless content inspection of uploaded ZIPs is implemented; most scanners will miss this because it is role-gated and requires authenticated file upload.
STEP 03

Trigger server-side extraction

The malicious archive is uploaded through the SCORM import workflow, after which the server extracts it. On vulnerable versions, archive members can escape the intended SCORM directory and land elsewhere on disk with the permissions of the Frappe app process.
Conditions required:
  • SCORM import feature is enabled and reachable to the attacker
  • The target is running Frappe LMS 2.50.0 or below
Where this breaks in practice:
  • Organizations not using SCORM imports are effectively not exposed to this path
  • Containerization and restrictive filesystem permissions can sharply limit where writes can land
Detection/coverage: Best signal is host telemetry: unexpected writes by bench, gunicorn, or worker processes outside /sites/*/public/scorm/. Web logs should show SCORM upload/import activity immediately before suspicious file creation.
STEP 04

Convert file write into business impact

Once arbitrary write is achieved, the attacker can pursue defacement, credential theft, persistent JavaScript injection, tampering with course content, or—on weaker deployments—overwrite files that become executable on restart. Weaponization tools after this point are generic post-exploitation methods, not something unique to Frappe LMS.
Conditions required:
  • Useful writable path outside the SCORM directory
  • A deployment where overwritten files are served, trusted, or executed
Where this breaks in practice:
  • Arbitrary file write does not automatically equal instant RCE
  • Least-privilege app users, immutable containers, and separated code/upload mounts can contain blast radius
Detection/coverage: EDR/FIM coverage is strong here if enabled: look for new or modified files in app code, site configs, templates, static assets, cron targets, or supervisor-managed paths.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo confirmed in-the-wild exploitation surfaced in the reviewed sources, and it is not KEV-listed per provided intel. That materially lowers urgency versus a similar unauthenticated file-write bug under active abuse.
Public PoC availabilityNo polished public exploit repo stood out, but the fix is transparent: GHSA-mxh7-g3r7-g96h plus PR #2274 are enough for a copycat to build a ZIP-slip PoC quickly.
EPSSProvided intel says 0.00052. A secondary aggregation at Tenable shows 0.00047; either way, this is extremely low threat-likelihood telemetry. Percentile was not authoritatively retrieved during review.
KEV statusNo. No CISA KEV deadline pressure based on the intel supplied with the case.
CVSS / impact modelNVD shows a GitHub CNA CVSS v4 base 9.4 / CRITICAL with 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 at NVD. I think that overstates reality because PR:L here is doing a lot of work: it implies a logged-in, authorized editor on a feature-specific path.
Affected versionsAuthoritative description says Frappe LMS 2.50.0 and below are affected, per NVD and the GitHub advisory.
Fixed versionFixed in 2.50.1 via path validation during SCORM extraction, per 2.50.1" target="_blank" rel="noopener">release v2.50.1. I found no distro backport evidence; Debian marks it NOT-FOR-US.
Exposure realityThis is a niche, self-hosted LMS product, not a broadly exposed edge appliance. I found no dependable internet-scale fingerprint/count data in public GreyNoise/Shodan/Censys-style telemetry during review, so exposure should be treated as limited but not zero. *That is an inference from weak public telemetry, not proof of safety.*
Disclosure timelineDisclosed 2026-05-20; the patch work itself was merged earlier on 2026-03-30 in PR #2274, then shipped in v2.50.1.
Reporter / discovererThe advisory credits @nickhefty as reporter in the GitHub security advisory.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to HIGH (7.8/10)

The decisive factor is the prerequisite chain: an attacker needs an authenticated course-editor account and access to the SCORM upload workflow before this bug matters. That sharply narrows the reachable population versus internet-facing pre-auth file-write bugs, but the remaining impact is still serious because arbitrary file write as the app user can become defacement, persistence, credential theft, or sometimes RCE.

HIGH Affected version range and fixed version
MEDIUM Likely impact ceiling on varied self-hosted deployments
HIGH Severity downgrade from unauthenticated-remote assumptions

Why this verdict

  • Authenticated and role-bound: exploitation requires a valid account with the course editing role, which implies insider abuse or a prior compromise stage; that is the main reason this is not CRITICAL.
  • Feature-gated exposure: only deployments that actually use SCORM imports and delegate content-authoring rights to semi-trusted users are meaningfully exposed, which narrows the reachable population well below generic web-app exposure.
  • Impact remains heavyweight: once reached, this is arbitrary file write by the application process, which can absolutely lead to high-consequence outcomes on weakly isolated hosts.

Why not higher?

This is not an unauthenticated edge exploit and not a wormable path. The attacker must already be inside the trust boundary with the right role, and that prerequisite should be stopped by MFA, SSO hygiene, PAM discipline, and least-privilege role assignment in many enterprises.

Why not lower?

Arbitrary file write is not a cosmetic bug. Even with the prerequisite friction, a compromised editor account on a vulnerable host can still tamper with served content, plant persistence, or escalate to code execution depending on filesystem layout and restart behavior.

05 · Compensating Control

What to do — in priority order.

  1. Restrict or disable SCORM uploads — If SCORM import is unused, turn it off; if it is needed, limit it to a tiny trusted author group. This directly removes the vulnerable code path and should be deployed within 30 days for a HIGH finding.
  2. Shrink course-editor privileges — Audit who actually has course editing rights and remove the role from broad instructor or contractor populations. Because this bug is role-gated, privilege cleanup is a first-order risk reducer and should be completed within 30 days.
  3. Harden filesystem boundaries — Run the Frappe process with least privilege and keep upload paths separate from code, configs, service definitions, and executable content; use container immutability or mount options where possible. This limits arbitrary write blast radius and should be enforced within 30 days.
  4. Alert on out-of-path writes — Create FIM/EDR detections for Frappe web or worker processes writing outside expected SCORM directories, especially under app code, sites/, templates, or served static paths. Detection engineering should be in place within 30 days.
  5. Tighten account assurance — Require MFA for course-authoring accounts and review recent sign-ins, especially for shared training/admin identities. Since the exploit begins with a trusted editor account, identity controls materially reduce reachable risk and should be enforced within 30 days.
What doesn't work
  • A WAF alone does not solve this well; the dangerous payload is inside an authenticated ZIP upload and the failure happens during server-side extraction.
  • A network perimeter blocklist is weak compensation because the attacker can be a legitimate internal or federated user.
  • Malware scanning of uploads is not enough; this is a path-validation failure, not a signature-based malicious-file problem.
  • Unauthenticated external scanning will miss most of the real exposure because the vulnerable path is role-gated behind login.
06 · Verification

Crowdsourced verification payload.

Run this on the Linux host or container that runs Frappe LMS, ideally from the bench root or with the bench root passed as an argument. Invoke it as bash verify_cve_2026_39405.sh /opt/frappe-bench or simply bash verify_cve_2026_39405.sh from the bench directory; it needs only read access to app metadata and, if available, the bench command.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# verify_cve_2026_39405.sh
# Checks whether a Frappe LMS installation is vulnerable to CVE-2026-39405
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

set -u

TARGET_VERSION="2.50.1"
BENCH_ROOT="${1:-$(pwd)}"

vercmp() {
  # returns: 0 equal, 1 greater, 2 less
  local a="$1" b="$2"
  if [[ "$a" == "$b" ]]; then
    return 0
  fi
  local first
  first=$(printf '%s\n%s\n' "$a" "$b" | sort -V | head -n1)
  if [[ "$first" == "$a" ]]; then
    return 2
  else
    return 1
  fi
}

normalize_version() {
  local v="$1"
  v="${v#v}"
  v="${v%%[-+~]*}"
  printf '%s' "$v"
}

extract_from_bench_version() {
  local out line v
  if ! command -v bench >/dev/null 2>&1; then
    return 1
  fi
  out=$(cd "$BENCH_ROOT" 2>/dev/null && bench version 2>/dev/null) || return 1
  while IFS= read -r line; do
    case "$line" in
      lms*|*" lms "*|*"lms "*)
        v=$(printf '%s' "$line" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
        if [[ -n "$v" ]]; then
          printf '%s' "$v"
          return 0
        fi
        ;;
    esac
  done <<< "$out"
  return 1
}

extract_from_pyproject() {
  local file line v
  for file in \
    "$BENCH_ROOT/apps/lms/pyproject.toml" \
    "$BENCH_ROOT/frappe-bench/apps/lms/pyproject.toml"; do
    [[ -f "$file" ]] || continue
    line=$(grep -E '^version\s*=\s*"[0-9]+\.[0-9]+\.[0-9]+"' "$file" 2>/dev/null | head -n1 || true)
    v=$(printf '%s' "$line" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
    if [[ -n "$v" ]]; then
      printf '%s' "$v"
      return 0
    fi
  done
  return 1
}

extract_from_git_tag() {
  local dir v
  for dir in \
    "$BENCH_ROOT/apps/lms" \
    "$BENCH_ROOT/frappe-bench/apps/lms"; do
    [[ -d "$dir/.git" ]] || continue
    v=$(git -C "$dir" describe --tags --abbrev=0 2>/dev/null | grep -Eo 'v?[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)
    if [[ -n "$v" ]]; then
      normalize_version "$v"
      return 0
    fi
  done
  return 1
}

VERSION=""
METHOD=""

if VERSION=$(extract_from_bench_version); then
  METHOD="bench version"
elif VERSION=$(extract_from_pyproject); then
  METHOD="pyproject.toml"
elif VERSION=$(extract_from_git_tag); then
  METHOD="git tag"
else
  echo "UNKNOWN - could not determine installed Frappe LMS version from bench, pyproject, or git metadata under: $BENCH_ROOT"
  exit 2
fi

VERSION=$(normalize_version "$VERSION")

if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  echo "UNKNOWN - parsed version '$VERSION' via $METHOD but it is not comparable"
  exit 2
fi

vercmp "$VERSION" "$TARGET_VERSION"
case $? in
  0)
    echo "PATCHED - detected Frappe LMS $VERSION via $METHOD (fixed version is $TARGET_VERSION)"
    exit 0
    ;;
  1)
    echo "PATCHED - detected Frappe LMS $VERSION via $METHOD (newer than fixed version $TARGET_VERSION)"
    exit 0
    ;;
  2)
    echo "VULNERABLE - detected Frappe LMS $VERSION via $METHOD (affected: $TARGET_VERSION and below per advisory wording)"
    exit 1
    ;;
esac

echo "UNKNOWN - unexpected version comparison failure"
exit 2
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: treat this as a HIGH because it is post-auth and role-gated, not because the impact is mild. Use the noisgate mitigation SLA to remove exposure within 30 days by restricting SCORM upload rights, shrinking course-editor access, and hardening writable paths; then use the noisgate remediation SLA to get every remaining Frappe LMS instance to 2.50.1 or later within 180 days. If you have internet-exposed LMS instances used by contractors, students, or other semi-trusted users, pull those to the front of the queue first.

Sources

  1. NVD CVE-2026-39405
  2. GitHub Security Advisory GHSA-mxh7-g3r7-g96h
  3. Frappe LMS release v2.50.1
  4. Patch PR #2274
  5. Frappe LMS repository README / deployment notes
  6. Tenable CVE page with EPSS snapshot
  7. Debian security tracker
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.