← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2025-22389 · CWE-434 · Disclosed 2025-01-04

Optimizely EPiServer

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

This is a bad package left in the mailroom, not a burglar kicking in the front door

CVE-2025-22389 is an unrestricted dangerous file upload issue in Optimizely CMS versions before 12.32.0. Public records describe the affected component inconsistently: the CVE/NVD text says EPiServer.CMS.Core, while Optimizely's own advisory says EPiServer.CMS.UI; either way, the flaw sits in the CMS media upload workflow and allows authenticated users to upload risky file types such as .html and .docm. The practical consequence is not server-side RCE by default; the harm happens when another user later opens the uploaded file.

The vendor-side reality is closer to Medium than High. Optimizely's advisory rates it Medium / 6.3, and that matches field conditions better than the 8.0 High CVSS vector in CISA-ADP because the chain needs a valid CMS account, usually create rights on media folders, and then user interaction from a second victim. That is a lot of friction for a 10,000-host patch queue, and it sharply limits both reachable population and blast radius.

"This is an editor-abuse problem, not an internet-breaks-open emergency."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Obtain a CMS editor foothold

The attacker first needs a valid Optimizely CMS account that can reach the edit interface. In practice this is a credential theft, SSO compromise, or insider scenario, not a one-packet internet exploit. Tooling is ordinary operator tradecraft: a browser session, stolen cookies, or credential reuse rather than a specialized exploit kit.
Conditions required:
  • Authenticated access to the CMS edit surface
  • Account has at least low privileges in CMS
  • Admin/editor plane is reachable from the attacker's network position
Where this breaks in practice:
  • This is post-auth by design
  • Many enterprises put the editor surface behind SSO, VPN, IP allowlists, or admin segmentation
  • MFA and conditional access reduce commodity abuse
Detection/coverage: External vuln scanners usually miss this because they cannot validate editor permissions or simulate authenticated media upload workflows. IAM, SSO, and admin portal logs are the best telemetry for step 1.
STEP 02

Upload a dangerous file through the media workflow

Using the CMS media upload path, the attacker uploads a file type that should have been blocked or more tightly validated. The weaponized payload is usually a malicious .html page or macro-enabled Office document prepared with standard tools such as Burp Suite, a browser, or Office-based payload builders. Optimizely's media docs show upload requires Create rights on the target folder, and auto-publish can depend on Publish rights.
Conditions required:
  • Target runs a vulnerable version before 12.32.0
  • Attacker has Create rights on a media folder or current page/block context
  • The deployment has not already restricted file extensions or content types
Where this breaks in practice:
  • Custom allowlists, upload validators, or middleware can already block .html, .docm, .svg, and similar risky formats
  • Some deployments require approval or manual publish before other users can reach the file
  • AV or content scanning on upload can catch common malware payloads
Detection/coverage: Good web logging can flag uploads of unusual extensions and uploads by low-volume editor accounts. DAST products rarely verify business-logic upload restrictions with real role context.
STEP 03

Get another user to open the file

The uploaded file must then be accessed by a victim user. For .html, the attacker typically sends an internal link or embeds a reference in normal CMS workflow; for .docm, the victim has to download and open the document in Office. The weaponized tool at this stage is the payload itself: browser-executed HTML/JavaScript or Office macro content.
Conditions required:
  • File is reachable to the intended victim
  • A second user opens, previews, or downloads the file
  • Business workflow makes the file look legitimate enough to trust
Where this breaks in practice:
  • This is user-interaction required
  • Modern browsers sandbox aggressively and may not give useful access unless the CMS/session model is weak
  • Office Protected View, macro blocking, email/web filtering, and user skepticism kill a lot of these chains
Detection/coverage: Web proxy, browser telemetry, and Office/EDR events are more useful than server scanners here. Look for first-seen internal downloads of risky media types from CMS asset paths.
STEP 04

Abuse the victim context for follow-on impact

If the victim opens the payload successfully, the attacker can attempt session theft, credential capture, social engineering, or malware delivery. The impact can be serious if the victim is a privileged CMS administrator, but it is still indirect client-side abuse, not a direct unauthenticated server takeover. Common follow-on tooling would be browser session theft scripts, phishing pages, or endpoint malware loaders.
Conditions required:
  • Victim has useful privileges or access
  • Payload survives browser or Office hardening
  • Session handling and cookie controls are weak enough to be exploitable
Where this breaks in practice:
  • Blast radius is bounded by who opens the file
  • EDR, browser isolation, protected cookies, and macro controls often stop the last mile
  • A successful outcome depends heavily on local enterprise hardening, not just the CMS bug
Detection/coverage: EDR is the control most likely to catch the real damage stage. Watch for child processes from Office, suspicious browser script activity, and unusual CMS admin actions after media access.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo known active exploitation in reviewed public sources. Not in CISA KEV, and OpenCVE's CISA-ADP SSVC enrichment marks Exploitation: none and Automatable: no.
Proof-of-concept availabilityNo public PoC or exploit repo identified in reviewed sources. That matters because this is already a workflow-heavy bug; lack of turnkey weaponization keeps it out of the commodity-attacker fast lane.
EPSSPrompt intel gives 0.00572. That's low-probability territory for near-term exploitation; I did not confirm an exact FIRST percentile from a primary source during review.
KEV statusNot KEV-listed as reviewed against the current CISA Known Exploited Vulnerabilities Catalog.
CVSS vector reality checkCVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H looks scary on paper, but the real dampeners are authenticated access, role/permission requirements, and user interaction. This is not unauthenticated pre-auth internet RCE.
Affected versionsPublic records say before 12.32.0. CVE/NVD text names EPiServer.CMS.Core, while Optimizely's advisory text names EPiServer.CMS.UI; defenders should treat CMS deployments carrying vulnerable 12.x package sets below 12.32.0 as in scope and verify actual package references.
Fixed version12.32.0. I found no distro backport guidance; this is a vendor package upgrade story, not an OS-package backport story.
Exposure and scanning dataI found no reliable product-specific GreyNoise, Shodan, or Censys census for this CVE. That absence itself is telling: there is no broad public telemetry showing mass scanning or exploit traffic pressure.
Disclosure timelineOptimizely's advisory is dated 2025-01-03; CVE/NVD publication is 2025-01-04; NVD added enrichment on 2025-05-20.
Researcher / reportingNo external finder credit was exposed in the reviewed vendor/CVE material. The public trail is essentially Optimizely advisory + MITRE/CISA/NVD metadata, not a detailed third-party research write-up.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.0/10)

The decisive factor is that this bug is post-auth and workflow-dependent: the attacker needs CMS access and then still has to get another user to open the uploaded payload. That sharply narrows exposed population compared with a true perimeter exploit, even though the eventual user impact can be ugly.

HIGH Requires authenticated access and user interaction
HIGH No KEV or credible active exploitation evidence in reviewed public sources
MEDIUM Exact affected package naming (`CMS.Core` vs `CMS.UI`) in public records

Why this verdict

  • Down from 8.0 because it is not pre-auth — the first prerequisite is a valid CMS account, which implies either prior compromise, insider access, or a separate identity failure.
  • Down again because upload rights are not universal — real Optimizely deployments gate media upload with folder/page permissions, so only a subset of authenticated users can even reach the dangerous path.
  • Down again because exploitation is user-assisted — the payload has to be opened by another user, and modern browsers, Office hardening, and EDR all add practical breakpoints.
  • Held at MEDIUM, not LOW, because the victim may be privileged — if a CMS admin opens the file, the attacker can pivot into session theft, credential capture, or malware delivery with meaningful business impact.

Why not higher?

There is no evidence here of unauthenticated server compromise, wormability, or mass-exploitation pressure. The exploit chain assumes prior access plus a second human action, which is exactly the kind of compounded friction that should push a nominally high CVSS down in operational patching priority.

Why not lower?

This is still a dangerous-file upload flaw in a business CMS, not a cosmetic bug. Enterprises often have many editors and contractors touching media workflows, and a successful lure against a privileged user can absolutely become a material security incident.

05 · Compensating Control

What to do — in priority order.

  1. Enforce a strict upload allowlist — Allow only business-required file types and explicitly block active content such as .html, .htm, .svg, .js, .docm, and similar risky formats. For a MEDIUM verdict there is no noisgate mitigation SLA; if you cannot patch immediately, deploy this in the next normal admin-plane change window and then move to the patch inside the 365-day remediation window.
  2. Require malware and content scanning on upload — Scan uploaded media before storage or publication, and quarantine files that trip signatures or policy. This is especially useful when editors legitimately upload Office documents. Again, no mitigation SLA — go straight to the 365-day remediation window, but this is worth turning on sooner anywhere the editor surface is internet-reachable.
  3. Turn off automatic publish for media where feasible — Force uploaded files through approval or manual publish so a malicious upload does not instantly become reachable by other users. That adds a human choke point to the attack path; for MEDIUM, schedule it in normal change control if your workflow tolerates it.
  4. Shrink who can upload media — Review Optimizely folder and page Create/Publish rights and strip upload capability from broad editor groups, contractors, and stale service accounts. Least privilege directly attacks the most important real-world amplifier for this CVE: too many people able to use the upload workflow.
  5. Harden the editor plane — Put CMS edit/admin access behind SSO, MFA, conditional access, VPN/IP allowlists, and better logging. This does not fix the bug, but it makes the required authenticated foothold harder to obtain and easier to investigate.
What doesn't work
  • A WAF alone will not save you here; the upload may look like a normal authenticated business action and the dangerous part is the downstream file handling.
  • Relying on browser download warnings is weak; .html may execute in-browser and Office warnings are routinely bypassed by determined users.
  • Perimeter vulnerability scanning alone is poor coverage because the issue sits behind authentication and role-aware CMS workflows.
06 · Verification

Crowdsourced verification payload.

Run this on the target application host or on a build artifact/SBOM workspace containing the CMS app files. Invoke it as python3 verify_cve_2025_22389.py /path/to/app and it needs only read access to the application directory; it checks *.deps.json, packages.lock.json, Directory.Packages.props, *.csproj, and packages.config for EPiServer.CMS.UI, EPiServer.CMS.Core, or EPiServer.CMS versions and prints VULNERABLE, PATCHED, or UNKNOWN.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# verify_cve_2025_22389.py
# Exit codes:
#   0 = PATCHED
#   1 = VULNERABLE
#   2 = UNKNOWN / could not determine

import json
import os
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

TARGET_PACKAGES = {
    'episerver.cms.ui',
    'episerver.cms.core',
    'episerver.cms'
}
FIXED = (12, 32, 0)


def normalize_name(name):
    return name.strip().lower()


def strip_prerelease(v):
    return re.split(r'[-+]', v, 1)[0]


def parse_version(v):
    if not v:
        return None
    v = v.strip()
    v = v.strip('[]()')
    v = strip_prerelease(v)
    m = re.match(r'^(\d+)\.(\d+)\.(\d+)', v)
    if not m:
        return None
    return tuple(int(x) for x in m.groups())


def version_lt(a, b):
    return a < b


def add_hit(hits, pkg, ver, source):
    hits.append({'package': pkg, 'version': ver, 'source': source})


def scan_deps_json(path, hits):
    try:
        data = json.loads(path.read_text(encoding='utf-8', errors='ignore'))
    except Exception:
        return

    # Newer .NET deps layout
    libs = data.get('libraries', {})
    for key in libs.keys():
        # format usually: PackageName/Version
        if '/' in key:
            name, ver = key.split('/', 1)
            if normalize_name(name) in TARGET_PACKAGES:
                add_hit(hits, name, ver, str(path))

    # Fallback walk
    def walk(obj):
        if isinstance(obj, dict):
            for k, v in obj.items():
                if isinstance(k, str) and '/' in k:
                    name, ver = k.split('/', 1)
                    if normalize_name(name) in TARGET_PACKAGES:
                        add_hit(hits, name, ver, str(path))
                walk(v)
        elif isinstance(obj, list):
            for i in obj:
                walk(i)

    walk(data)


def scan_packages_lock(path, hits):
    try:
        data = json.loads(path.read_text(encoding='utf-8', errors='ignore'))
    except Exception:
        return

    deps = data.get('dependencies', {})
    for tfm, pkgs in deps.items():
        if isinstance(pkgs, dict):
            for name, meta in pkgs.items():
                if normalize_name(name) in TARGET_PACKAGES and isinstance(meta, dict):
                    ver = meta.get('resolved') or meta.get('version')
                    if ver:
                        add_hit(hits, name, ver, str(path))


def scan_packages_config(path, hits):
    try:
        root = ET.parse(path).getroot()
    except Exception:
        return
    for pkg in root.findall('.//package'):
        name = pkg.attrib.get('id', '')
        ver = pkg.attrib.get('version', '')
        if normalize_name(name) in TARGET_PACKAGES and ver:
            add_hit(hits, name, ver, str(path))


def scan_xml_project(path, hits):
    try:
        root = ET.parse(path).getroot()
    except Exception:
        return

    # Handle XML namespaces if present
    ns = ''
    if root.tag.startswith('{'):
        ns = root.tag.split('}')[0] + '}'

    # PackageReference entries
    for pr in root.findall(f'.//{ns}PackageReference'):
        name = pr.attrib.get('Include') or pr.attrib.get('Update') or ''
        ver = pr.attrib.get('Version', '')
        if not ver:
            version_elem = pr.find(f'{ns}Version')
            if version_elem is not None and version_elem.text:
                ver = version_elem.text.strip()
        if normalize_name(name) in TARGET_PACKAGES and ver:
            add_hit(hits, name, ver, str(path))

    # Central package management entries
    for pv in root.findall(f'.//{ns}PackageVersion'):
        name = pv.attrib.get('Include') or ''
        ver = pv.attrib.get('Version', '')
        if normalize_name(name) in TARGET_PACKAGES and ver:
            add_hit(hits, name, ver, str(path))


def main():
    if len(sys.argv) != 2:
        print('UNKNOWN: usage: python3 verify_cve_2025_22389.py /path/to/app')
        sys.exit(2)

    base = Path(sys.argv[1])
    if not base.exists():
        print(f'UNKNOWN: path not found: {base}')
        sys.exit(2)

    hits = []
    patterns = ['*.deps.json', 'packages.lock.json', 'packages.config', '*.csproj', 'Directory.Packages.props']

    for pattern in patterns:
        for path in base.rglob(pattern):
            name = path.name.lower()
            if name.endswith('.deps.json'):
                scan_deps_json(path, hits)
            elif name == 'packages.lock.json':
                scan_packages_lock(path, hits)
            elif name == 'packages.config':
                scan_packages_config(path, hits)
            elif name.endswith('.csproj') or name == 'directory.packages.props':
                scan_xml_project(path, hits)

    if not hits:
        print('UNKNOWN: no Optimizely CMS package references found')
        sys.exit(2)

    parsed = []
    for h in hits:
        pv = parse_version(h['version'])
        if pv is not None:
            parsed.append((pv, h))

    if not parsed:
        print('UNKNOWN: Optimizely packages found, but versions were not parseable')
        for h in hits:
            print(f"  {h['package']} {h['version']} @ {h['source']}")
        sys.exit(2)

    vulnerable = [h for pv, h in parsed if version_lt(pv, FIXED)]
    patched = [h for pv, h in parsed if not version_lt(pv, FIXED)]

    # Prefer vulnerable if any in-scope package is below fixed version
    if vulnerable:
        print('VULNERABLE: found Optimizely CMS package versions below 12.32.0')
        for h in vulnerable:
            print(f"  {h['package']} {h['version']} @ {h['source']}")
        sys.exit(1)

    if patched:
        print('PATCHED: found Optimizely CMS package versions at or above 12.32.0')
        for h in patched:
            print(f"  {h['package']} {h['version']} @ {h['source']}")
        sys.exit(0)

    print('UNKNOWN: package references found but state could not be determined')
    sys.exit(2)


if __name__ == '__main__':
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not treat this like a perimeter fire drill. First, inventory every Optimizely CMS deployment carrying EPiServer.CMS.* packages below 12.32.0, then separate internet-reachable editor surfaces from internal-only ones and verify who actually has upload rights. For a MEDIUM noisgate verdict there is no noisgate mitigation SLA — go straight to the 365-day remediation window; if you want belt-and-suspenders protection sooner, tighten upload allowlists and media permissions in the next normal change window, then complete the actual package upgrade by the noisgate remediation SLA of 365 days.

Sources

  1. Optimizely advisory CMS-2025-03
  2. Optimizely CMS security announcements
  3. NVD record for CVE-2025-22389
  4. OpenCVE enriched record with CISA-ADP SSVC
  5. Optimizely docs: Media upload workflow and permissions
  6. Optimizely docs: access rights for upload and auto-publish
  7. CISA Known Exploited Vulnerabilities Catalog
  8. OSV record for affected range and fixed version
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.