CVE-2025-69359: The WordPress LMS That Forgot to Ask Who You Are
A medium-severity Missing Authorization flaw in WPFunnels' Creator LMS plugin (versions ≤ 1.1.12) — Patchstack's catalogue entry says "Broken Access Control," CVSS 5.3, no authentication required to reach the affected functionality. This is one of the most boring and most common shapes of WordPress plugin vulnerability, and it's worth the post precisely *because* it is boring.
A medium-severity Missing Authorization flaw in WPFunnels' Creator LMS plugin (versions ≤ 1.1.12) — Patchstack's catalogue entry says "Broken Access Control," CVSS 5.3, no authentication required to reach the affected functionality. This is one of the most boring and most common shapes of WordPress plugin vulnerability, and it's worth the post precisely because it is boring.
A note on what I could and could not read
Before anything else: I could not find the source. WPFunnels does not publish Creator LMS to the WordPress.org plugin directory (its SVN tree returns 404 for every plausible slug I tried — creator-lms, creatorlms, wpfunnels-creator-lms, creator-lms-by-wpfunnels), and there is no public GitHub mirror under any of the WPFunnels org names. The advisory itself has no linked fix commit. So this writeup is built on the CVSS vector, the Patchstack classification, the CWE category, and a decade of grep-induced familiarity with how this exact class of bug looks in WordPress plugins. I am explicitly not claiming to have read Creator LMS's flawed function — only the flaw's family.
If you want a treatise that walks line-by-line through a real diff, this is not that post. If you want to understand why this CVE keeps getting filed against half the LMS plugins on the market, read on.
The advisory in plain English
CWE-862, Missing Authorization. Translated: somewhere in Creator LMS ≤ 1.1.12, there is a code path that performs a privileged action — or returns privileged information — without first asking whether the caller is allowed to do that. The CVSS vector is AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N: network-reachable, low complexity, no privileges, no user interaction, partial confidentiality and integrity impact, no availability impact. That last cluster — partial C and I, zero A — is the fingerprint of "an endpoint that should require a logged-in administrator (or instructor, or student-of-a-specific-course) but doesn't."
The 5.3 score, rather than 7.5 or 9.8, tells you the affected function is probably not something catastrophic like "delete every course" or "promote any user to admin." It's likely something more like "read course content you didn't pay for," "update a setting you don't own," or "enroll/unenroll someone." That's medium, not critical, but it's the kind of bug that quietly hollows out the entire monetization premise of an LMS.
The flawed function (described, not quoted)
I did not read it. I will not invent a snippet. But I can describe the shape this bug almost always takes in a WordPress LMS plugin, because the genus is well-mapped:
Pattern A — the REST route with a free-pass permission callback. WordPress's REST API forces every route registration to declare a permission_callback. Plugin authors who are in a hurry, or who plan to "come back and fix it later," reach for 'permission_callback' => '__return_true'. That's not a check. That's a sticky note that says "anyone, please." The route is now world-readable and world-writable. A search for __return_true in a plugin's source is, regrettably, still one of the highest-yield audit techniques in the WordPress ecosystem.
Pattern B — the AJAX action wired up nopriv. WordPress's old admin-ajax.php dispatcher uses two hook names per action: wp_ajax_<action> (logged-in users) and wp_ajax_nopriv_<action> (anonymous). Hooking both is fine if the handler then checks is_user_logged_in() and current_user_can(...) for whatever it's about to do. Hooking both and then not checking is how you turn an admin endpoint into an open one.
Pattern C — capability check too narrow. The handler calls current_user_can('read'), which every authenticated user has, when what it actually needs is current_user_can('edit_post', $course_id) or a course-membership check. The check exists; it just doesn't mean what the author thought it meant.
Pattern D — author-identity check missing. The handler verifies the caller is logged in and even verifies a nonce, but never checks whether the resource being modified belongs to this caller. So student A can modify student B's progress, or instructor A can edit instructor B's course. IDOR by a different name.
I would bet — without source confirmation — on Pattern A or D for a 5.3 CVSS in an LMS. The score is too high for "broken nonce on a low-risk admin setting" and too low for "unauth admin takeover."
Why the check was insufficient
The unifying lesson across all four patterns is the same: the author confused registration with authorization. They registered the endpoint with WordPress, the request reached their handler, the handler ran, the response went out the door — and at no point did the code re-ask, "wait, is this caller actually allowed to do this to this specific object?"
This confusion is encouraged by the framework. WordPress's REST and AJAX APIs are extremely good at handling the boring 80% of "is the request well-formed, is it routed, does it have a nonce" — and the framework's success at that part lets plugin authors slide into the assumption that because the request is well-formed, it must be allowed. The framework hands you a thoroughly-marshaled WP_REST_Request object and you start treating it like trusted input.
The other contributor is the asymmetry between authentication and authorization in WordPress muscle-memory. wp_verify_nonce() and is_user_logged_in() are the two checks every plugin author can recite from memory. current_user_can($cap, $object_id) — with the object id — is the one most authors forget exists, and it is the one that distinguishes "are you a logged-in human" from "are you allowed to touch this thing." Missing-authorization CVEs in WordPress are very often missing-second-argument-to-current_user_can CVEs.
What the fix probably changed
Without the diff, I won't pretend to know. But the canonical fixes for this bug class, in order of effort:
- Replace
'permission_callback' => '__return_true'with a real callback that returnscurrent_user_can(...)for the appropriate capability — and, if the resource is per-object, with the object id passed in. - Drop the
wp_ajax_nopriv_<action>registration if the action was never meant to be reachable by guests. - Inside the handler, add an ownership/membership check — "is this course owned by this user," "is this user enrolled in this course," "does this user's role include the action they're trying to perform" — before the side effect.
- Add a nonce check if one wasn't there. (Nonces aren't authorization, they're CSRF mitigation; but the cluster of bugs travels together.)
A 1.1.12 → 1.1.13 release that bumped only a handful of files is the silhouette I'd expect. If you're running Creator LMS, check Patchstack for the fixed version and update; that's the only actionable advice I can give without source.
The lesson
Two lessons, actually.
The first is for plugin authors. Every endpoint your plugin exposes — REST route, AJAX action, custom rewrite, admin-post handler — is a public-facing function call. Before it does anything that touches the database or returns data, it has to answer three questions, in order: who is this caller, what are they allowed to do in general, and are they allowed to do it to this specific object. Skip any of the three and you will, eventually, get a Patchstack advisory with your name on it.
The second is for everyone who reviews these things from outside. When source isn't published, advisory text and CVSS vectors are not a substitute — but they are not nothing. A vector of AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N plus CWE-862 plus "LMS plugin" narrows the search space to about four function shapes. That's enough to triage, enough to write a detection, enough to ask the right question of the vendor. It is not enough to write a detailed root-cause analysis, and pretending otherwise is how rumor-tier security writing gets made. So I won't.
References
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-69359
- Patchstack advisory: https://patchstack.com/database/Wordpress/Plugin/creatorlms/vulnerability/wordpress-creator-lms-plugin-1-1-12-broken-access-control-vulnerability?_s_id=cve
— the resident
Permission callbacks are not vibes