critical Threat analysis

Widget Factory Joomla Content Editor CVE-2026-48907: KEV Unauthenticated Profile Upload to PHP RCE

CISA added CVE-2026-48907 to KEV on 2026-06-16. JCE 2.9.99[.]5 and 2.9.99[.]6 fix an unauthenticated editor-profile upload flaw that can lead to PHP code execution on Joomla sites.

#joomla#php#cisa-kev#rce#web-app
On this page 0% read

    Executive Summary

    Widget Factory’s Joomla Content Editor (JCE) has a critical unauthenticated access-control flaw, tracked as CVE-2026-48907. The vendor says the attack path lets an unauthenticated actor create a new editor profile, grant upload permission, and then upload executable PHP. The vendor also states that active exploitation is already public and automated [1].

    CISA added the vulnerability to the Known Exploited Vulnerabilities catalog on 2026-06-16 with a due date of 2026-06-19 [2]. JCE’s changelog shows the vulnerable logic was fixed in 2.9.99[.]5 on 2026-06-03, with 2.9.99[.]6 on 2026-06-08 adding further hardening [3]. Treat any internet-facing JCE deployment below 2.9.99[.]5 as presumed exposed until the exact installed version is verified [1][3].

    Key Facts

    Cve: CVE-2026-48907

    Vendor: Widget Factory / JCE

    Product: Joomla Content Editor

    Vulnerability Class: improper access control leading to unauthenticated profile upload and PHP execution

    Cwe: CWE-284

    Vendor Severity: Critical

    Kev Added: 2026-06-16

    Kev Due Date: 2026-06-19

    Fixed Versions:

    • 2.9.99[.]5 or later
    • 2.9.99[.]6 recommended for added hardening

    High Value Evidence:

    • JCE version output from the Joomla administrator panel or package inventory
    • Web server access logs around index[.]php?option=com_jce&task=profiles.import
    • Filesystem review of images/, media/, and tmp/ for unexpected PHP or XML-wrapped PHP payloads

    Evidence Assessment

    • confirmed: The vendor says JCE 2.9.99[.]5 patched the critical flaw in all earlier versions and 2.9.99[.]6 added hardening [1][3].
    • confirmed: The attack path is unauthenticated, creates a new editor profile, and enables upload of executable PHP [1][4].
    • confirmed: CISA added CVE-2026-48907 to KEV on 2026-06-16 with a 2026-06-19 due date [2].
    • confirmed: The JCE advisories say the attack is actively exploited and public exploit code exists [1].
    • not_observed: Public sources in this packet do not identify a victim list, a campaign name, or a separate malware family.

    Impact Determination

    ClassificationCriteriaRequired evidenceHandling decision
    Confirmed compromiseAccess logs show profiles.import activity followed by new editor profiles, unexpected PHP uploads, or execution of uploaded code.Access logs, file timestamps, webshell contents, and process history.Isolate the host, preserve logs, remove the rogue profile, and rebuild from a known-good backup.
    Presumed exposedThe site runs JCE below 2.9.99[.]5 and the admin surface is reachable.Package version, plugin inventory, and exposure scan.Upgrade to 2.9.99[.]6 or later, then review logs and files.
    Potentially exposedJCE is installed but version or reachability is unknown.Inventory and network scan.Collect exact version and exposure evidence immediately.
    Not exposedJCE is absent or already on a fixed version and no suspicious profile activity exists.Version evidence and negative log/file review.Document closure and keep credentials monitored.
    UnknownVersion, log, or file evidence is missing.Named gap and owner.Keep the site in high-priority triage until evidence is recovered.

    Timeline

    • 2026-06-03: JCE 2.9.99[.]5 ships and fixes the critical access-control flaw [3].
    • 2026-06-08: JCE 2.9.99[.]6 ships with additional hardening [3].
    • 2026-06-12: The vendor publishes a security-update post with response guidance and confirms active exploitation [1].
    • 2026-06-16: CISA adds CVE-2026-48907 to KEV [2].

    Technical Analysis

    The attack is simple but high impact: unauthenticated requests create an editor profile that permits executable file uploads, then the attacker uploads PHP into a writeable location and executes it through the web server [1][4]. The vendor specifically tells defenders to look for unauthenticated requests to index[.]php?option=com_jce&task=profiles.import, suspicious editor profiles, and PHP files in upload directories [1].

    The response priority is version-first, cleanup-second. If the vulnerable editor remains reachable, removing the rogue profile alone is not enough because the same automated attack can recreate access before cleanup is complete [1][3].

    Applicability Decision

    • Developer endpoint / workstation module: applicable only if administrators manage Joomla from a local workstation that may hold hosting credentials or backup access [1][3].
    • Cloud / CI-CD / GitHub / browser modules: not applicable for the primary attack path [1][3].
    • Hosting control-plane module: applicable if the same credentials manage the site, its backups, or its file-transfer tooling [1][3].

    Detection and Hunting

    Hunt Manifest: widget-factory-joomla-content-editor-cve-2026-48907-kev-hunt-1

    • Title: local repository and exported telemetry scope
    • Question: Does the telemetry scope contain patterns associated with CVE-2026-48907 exploitation on a Joomla site?
    • Telemetry Family: process
    • Telemetry Context: web-server logs and filesystem export
    • Positive Signal: Indicators of compromise matched in telemetry
    #!/usr/bin/env python3
    
    import argparse
    import json
    from pathlib import Path
    
    PROFILE_IMPORT = "index.php?option=com_jce&task=profiles.import"
    DOMAINS = {"joomlacontenteditor.net", "cisa.gov", "nvd.nist.gov"}
    URLS = {
      "https://www.joomlacontenteditor.net/news/jce-security-update-and-a-free-patch-for-older-sites",
      "https://www.joomlacontenteditor.net/support/changelog/editor",
      "https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-48907",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-48907",
    }
    SUSPICIOUS_DIRS = {"images", "media", "tmp"}
    FILE_PATTERNS = {"images/*.php", "media/*.php", "tmp/*.php"}
    SUSPICIOUS_SUFFIXES = {".php", ".phtml", ".php5", ".php7", ".phar"}
    FIXED_VERSIONS = {"2.9.99.5", "2.9.99.6"}
    
    
    def scan(root: Path) -> dict[str, list[str]]:
      log_hits: list[str] = []
      file_hits: list[str] = []
      version_hits: list[str] = []
      source_hits: list[str] = []
      for path in root.rglob("*"):
        if path.is_dir() or any(part in {".git", "node_modules", "vendor", "dist"} for part in path.parts):
          continue
        try:
          text = path.read_text(errors="ignore")
        except Exception:
          text = ""
        if PROFILE_IMPORT in text:
          log_hits.append(f"{path}: {PROFILE_IMPORT}")
        for domain in DOMAINS:
          if domain in text:
            source_hits.append(f"{path}: {domain}")
        for url in URLS:
          if url in text:
            source_hits.append(f"{path}: {url}")
        for version in FIXED_VERSIONS:
          if version in text:
            version_hits.append(f"{path}: {version}")
        rel = path.relative_to(root)
        if any(rel.match(pattern) for pattern in FILE_PATTERNS):
          file_hits.append(f"{path}: suspicious PHP-like upload path")
        elif path.suffix.lower() in SUSPICIOUS_SUFFIXES and any(part in SUSPICIOUS_DIRS for part in path.parts):
          file_hits.append(f"{path}: suspicious PHP-like upload path")
      return {
        "log_hits": sorted(set(log_hits)),
        "file_hits": sorted(set(file_hits)),
        "version_hits": sorted(set(version_hits)),
        "source_hits": sorted(set(source_hits)),
      }
    
    
    def main() -> int:
      parser = argparse.ArgumentParser(description=__doc__)
      parser.add_argument("root", nargs="?", default=".", help="filesystem or log root to scan")
      parser.add_argument("--out", default="jce-scope", help="output directory")
      args = parser.parse_args()
    
      root = Path(args.root).expanduser().resolve()
      out = Path(args.out).expanduser().resolve()
      out.mkdir(parents=True, exist_ok=True)
    
      result = scan(root)
      (out / "scan-summary.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
      print(json.dumps({"scanned_root": str(root), **result}, indent=2))
      return 0
    
    
    if __name__ == "__main__":
      raise SystemExit(main())

    Remediation and Closure

    1. Upgrade JCE to 2.9.99[.]6 or later and verify the installed version in the Joomla admin or package inventory [1][3].
    2. Delete any editor profile you did not create and confirm the upload permissions on every remaining profile [1].
    3. Remove uploaded PHP or XML-wrapped PHP files from images/, media/, and tmp/ and check timestamps before deleting anything else [1].
    4. Change administrator, database, and hosting/FTP credentials because the attack path is designed to reach secrets after code execution [1].
    5. Preserve logs before cleanup so you can reconstruct when profiles.import first appeared and whether the same account was reused elsewhere [1].

    Close the case only after the site is patched, the rogue profile is removed, files are cleaned, and the log review shows no additional malicious requests.

    Sources

    1. JCE security update and a free patch for older sites - Role: DIRECT_SOURCE - Impact: Confirms active exploitation, the profile-import attack path, and the recommended response sequence.
    2. CISA KEV catalog entry for CVE-2026-48907 - Role: GOVERNMENT_SOURCE - Impact: Confirms known exploitation and the 2026-06-19 remediation due date.
    3. JCE changelog for editor releases - Role: DIRECT_SOURCE - Impact: Shows 2.9.99[.]5 and 2.9.99[.]6 fix and harden the flaw.
    4. NVD CVE-2026-48907 - Role: PRIMARY_REFERENCE - Impact: Describes the unauthenticated profile-creation and PHP-execution behavior.

    IOC Clipboard

    2 IOCs
    Defang IOCs
    domain index.php index[.]php
    domain profiles.import profiles[.]import