CVE-2026-104798: My Calendar 3.8.7 Lets a Search String Call PHP - Floor 3.8.8

The Situation
Joe Dolson's My Calendar plugin is on wordpress.org as slug my-calendar. The plugin API this morning returned version 3.8.8, active_installs 20000 (the directory's 20k bucket), last updated 2026-10-14 12:53pm GMT, tested to 7.1.3, requires WP 6.5 / PHP 7.4.[4]
Wordfence Intelligence (CNA) lists CVE-2026-104798, CVSS 9.8 Critical (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), CWE-269, researcher daroo, publicly published 15 October 2026, last updated 16 October 2026. Affected ≤ 3.8.7 on the 3.8 line, plus older lines ≤ 3.4.24 / 3.5.21 / 3.6.17 / 3.7.17. Patched 3.4.25, 3.5.22, 3.6.18, 3.7.18, 3.8.8.[1][2]
NVD has an object this morning (totalResults 1, published 2026-10-16T05:17:28.580Z). The 9.8 is Wordfence's CVSS 3.1, also shown as NVD cvssMetricV31. CVE.org JSON is PUBLISHED, assigner Wordfence, datePublished 2026-10-16T04:27:04.876Z.[2][3]
Their claim, not ours: the public mcs GET parameter is reflected into post content before WordPress core's do_shortcode filter. An unauthenticated visitor injects a nested [my_calendar above=...] shortcode. mc_generate_calendar_nav() splits above on commas and dispatches each token with call_user_func() if function_exists() is true - no auth, no capability check, no callable allowlist. Wordfence says one chain is edit_user(): PHP coerces the shortcode $params array to integer 1, so user ID 1 (the first administrator) is overwritten. They say the auto-generated /my-calendar/ page is public by default on plugin activation.[1][2]
Vendor changelog (3.8.8): Security: Fixes a severe unauthenticated privilege escalation flaw. Props @daroo and Wordfence for responsible disclosure. No CVE ID and no CVSS on that page.[4]
CISA KEV catalog 2026.10.16 (count 1748, dateReleased 2026-10-16T17:15:48.8752Z) does not list CVE-2026-104798. Newest dateAdded today: BeyondTrust CVE-2026-102764 and Fortinet CVE-2025-61624 - not WordPress.[8]
What the bug actually is
We re-read tags/3.8.7 and tags/3.8.8 from wordpress.org SVN this morning. Wordfence's sink is in my-calendar-navigation.php. 3.8.7 (same shape as the 3.8.6 lines Wordfence cites) does this:[1][5]
$available = array( 'nav', 'toggle', 'jump', 'print', 'timeframe', 'key', 'feeds', 'exports', 'categories', 'locations', 'access', 'search' );
if ( '' !== $above ) {
$mc_toporder = array_map( 'trim', explode( ',', $above ) );
}
foreach ( $mc_toporder as $value ) {
if ( 'none' !== $value && in_array( $value, $used, true ) && in_array( $value, $available, true ) ) {
$value = trim( $value );
$mc_topnav .= ${$value};
}
if ( ! in_array( $value, $available, true ) && $value && 'none' !== strtolower( $value ) ) {
if ( function_exists( $value ) ) {
$mc_topnav .= call_user_func( $value, $params );
}
}
}
The $available list is only used for the named nav widgets. Any other comma-separated token that names a loaded PHP function is called with $params. The same pattern exists on the bottom nav (call_user_func at 3.8.7 lines 248 and 260). We did not execute it.[5]
3.8.8 deletes that second branch. The foreach now only concatenates $available widgets. call_user_func / function_exists are gone from mc_generate_calendar_nav(). That matches the vendor "privilege escalation" line. We did not prove every other call site is clean; we compared this function.[6]
Wordfence's entry is the public mcs query arg. In 3.8.7, mc_show_search_results() (hooked to the_content) reads $_GET['mcs'] through sanitize_text_field. Wordfence's write-up is that the value is still reflected into post content before do_shortcode. sanitize_text_field is not a shortcode allowlist. We did not craft a query string to confirm reflection.[1][5]
Do not treat this as "unauthenticated RCE via eval." Wordfence describes arbitrary loaded PHP function dispatch, then a credential overwrite of user ID 1. That is site takeover if their chain holds. It is not a claimed in-the-wild campaign.[1]
Hunt the fleet - plugin version, then user ID 1
# one site
wp plugin get my-calendar --field=version --allow-root
wp plugin list --name=my-calendar --fields=name,status,version --allow-root
# floor on the 3.8 line
wp plugin update my-calendar --allow-root
wp plugin get my-calendar --field=version --allow-root # want 3.8.8 or later
Stuck on an older 3.x line? Wordfence's patched versions are 3.4.25 / 3.5.22 / 3.6.18 / 3.7.18 / 3.8.8. Do not jump a frozen 3.5 site to 3.8 without a staging pass; do jump it off ≤ 3.5.21.[1]
# shared hosting: every account that has the slug
find /home /var/www -path '*/wp-content/plugins/my-calendar/my-calendar.php' 2>/dev/null
After the update, the plugin patch does not rewind a password that already changed. Wordfence's takeover path is user ID 1. Check even on patched sites:[1]
wp user get 1 --fields=ID,user_login,user_email,user_registered,roles --allow-root
wp user list --role=administrator --fields=ID,user_login,user_registered,user_email --allow-root
If user 1's email, login, or password hash is not what you expect, rotate that account and every other administrator before you argue about versions.
Cannot patch immediately (mitigations, not a substitute):
- Disable the plugin:
wp plugin deactivate my-calendar --allow-root. - If you must keep events up, unpublish the auto-generated
/my-calendar/page and any other page with[my_calendar]until 3.8.8 is on disk. Wordfence says that page is the trivial reachability path. We did not inventory every shortcode host.[1] - A WAF rule is not a version bump and does not restore user 1. Wordfence Intelligence this morning did not list a Premium/Free WAF date on the record we pulled.[1]
Honesty / What We Could Not Verify
- We did not exploit this. No
mcsquery was sent. Noedit_user()call was made. No password was changed. Sink text is from SVN tags 3.8.7 / 3.8.8 and Wordfence/NVD's description. - The
edit_user($params)coercion is Wordfence's chain, not something we reproduced. PHP'sedit_user()does type-juggle its first argument; whether that chain is reachable from a realmcsvalue is unverified here. sanitize_text_fieldonmcsis in 3.8.7 search code. Wordfence still calls the parameter unsanitized for shortcode purposes. Unresolved experimentally.- 20,000 is the wordpress.org API
active_installsbucket this morning, not a census we ran. Wordfence's record does not print an install count on the Intelligence page we pulled.[1][4] - Not on CISA KEV as of catalog 2026.10.16 / count 1748.[8]
- Not claimed in-the-wild. Wordfence's Intelligence page does not report observed exploitation. We are not inventing a campaign.
- Vendor page names no CVE and no CVSS. Only "severe unauthenticated privilege escalation" + daroo/Wordfence.[4]
- Wordfence trac cites tags/3.8.6. We read 3.8.7 (still vulnerable per CNA) and 3.8.8 (patched). Changeset 3746389 is the Wordfence/NVD reference; we used tagged files rather than the changeset HTML.[1][7]
- My Calendar Pro / My Tickets. wordpress.org slug is free
my-calendar. We did not inventory Joe Dolson's paid add-ons. - RCE ≠ this CVE. Wordfence describes function dispatch + admin credential overwrite, not a webshell upload.
What this is not
- Not CVE-2026-93953 (Backuply restore SQLi). Different plugin, needs an admin Restore.
- Not CVE-2026-39919 (Ghostscript PDF thumbnail heap overflow).
- Not CVE-2026-85097 (Bricksforge unauth PHP upload, already exploited per Patchstack).
- Not CVE-2026-109515 (MailOptin
form_custom_field_mappingsaccount takeover) or CVE-2026-109447 (User Profile Builder legacyrolefield). Those dropped the same NVD morning; smaller or more conditional. - Not a cPanel / Plesk / LiteSpeed / Proxmox KEV. Today's new KEV rows are BeyondTrust and Fortinet.
- Not "every My Calendar site is owned." Wordfence's own note: the sink is
call_user_funcon a function name taken fromabove. The site has to still be on ≤ 3.8.7 (or the matching older line).
Sources
[1] https://www.wordfence.com/threat-intel/vulnerabilities/id/e8aa6ee7-525e-4458-9f5b-1e729afe448c?source=cve - Wordfence Intelligence CVE-2026-104798 [2] https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-104798 - NVD CVE-2026-104798 [3] https://cveawg.mitre.org/api/cve/CVE-2026-104798 - CVE.org JSON CVE-2026-104798 [4] https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request%5Bslug%5D=my-calendar - wordpress.org plugin API my-calendar [5] https://plugins.svn.wordpress.org/my-calendar/tags/3.8.7/my-calendar-navigation.php - My Calendar 3.8.7 nav (call_user_func) [6] https://plugins.svn.wordpress.org/my-calendar/tags/3.8.8/my-calendar-navigation.php - My Calendar 3.8.8 nav (allowlist only) [7] https://plugins.trac.wordpress.org/changeset?old=3746389@my-calendar&new=3746389@my-calendar - My Calendar SVN changeset 3746389 [8] https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json - CISA KEV JSON catalog 2026.10.16