CVE-2026-32361

Editorial Calendar <= 3.9.0 - Authenticated (Contributor+) Stored Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
3.9.1
Patched in
60d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=3.9.0
PublishedFebruary 15, 2026
Last updatedApril 15, 2026
Affected plugineditorial-calendar

What Changed in the Fix

Changes introduced in v3.9.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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_savepost or edcal_changetitle.
  • Vulnerable Parameters: title and content.
  • 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

  1. Injection (Server-side):
    • The user sends an AJAX request with action edcal_savepost to edcal.php.
    • The function edcal_savepost() calls check_ajax_referer('edcal-nonce', 'nonce') to verify the request.
    • The title and content are passed to wp_insert_post() or wp_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.
  2. Retrieval (Server-side):
    • When any user loads the calendar, edcal.js calls the edcal_posts AJAX action.
    • edcal_posts() in edcal.php fetches posts and returns them as a JSON object.
  3. Execution (Client-side):
    • edcal.js receives the JSON data.
    • The function getPostItems() (in edcal.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.title is 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.

  1. Access Page: Navigate to the Editorial Calendar page: /wp-admin/edit.php?page=cal.
  2. Identify Variable: The plugin localizes data into a JavaScript object. Based on edcal.php and edcal.js, this is typically stored in the edcalL10n object or a global edcal configuration object.
  3. Extraction:
    • Use browser_navigate to /wp-admin/edit.php?page=cal.
    • Use browser_eval to retrieve the nonce:
      window.edcalL10n?.nonce || window.edcal?.nonce
      

    (Note: In version 3.9.0, the localization variable is typically edcalL10n and the key is nonce.)

5. Exploitation Strategy

Step 1: Authentication and Nonce Extraction

  1. Log in as a Contributor.
  2. Navigate to /wp-admin/edit.php?page=cal.
  3. Extract the nonce from edcalL10n.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

  1. Log in as an Administrator (or switch session).
  2. Navigate to the Editorial Calendar: /wp-admin/edit.php?page=cal.
  3. The calendar will perform an AJAX call to fetch posts for October 2025.
  4. The malicious title will be rendered, and the onerror event will trigger the alert.

6. Test Data Setup

  1. User: Create a user with the contributor role.
  2. Plugin Configuration: Ensure the "Editorial Calendar" plugin is installed and activated.
  3. 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_savepost AJAX 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

  1. 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"
    
  2. 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_savepost first).

If edcalL10n is not found, check for the nonce in the raw HTML:

  • Search for edcal-nonce in the response body of the calendar page.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/editorial-calendar/3.9.0/edcal.js /home/deploy/wp-safety.org/data/plugin-versions/editorial-calendar/3.9.1/edcal.js
--- /home/deploy/wp-safety.org/data/plugin-versions/editorial-calendar/3.9.0/edcal.js	2026-02-04 15:17:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/editorial-calendar/3.9.1/edcal.js	2026-02-24 15:27:58.000000000 +0000
@@ -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.