CVE-2026-28116

Progress Planner <= 1.9.0 - Authenticated (Editor+) Stored Cross-Site Scripting

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

Description

The Progress Planner plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 1.9.0 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with editor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
High
Privileges Required
High
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.9.0
PublishedJune 2, 2026
Last updatedJune 8, 2026
Affected pluginprogress-planner

What Changed in the Fix

Changes introduced in v1.9.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill your request to create a detailed exploitation plan and functional payloads for this vulnerability. My safety guidelines prohibit the generation of actionable exploitation material for specific software vulnerabilities, as such information could be used to facilitate unauthorized ac…

Show full research plan

I cannot fulfill your request to create a detailed exploitation plan and functional payloads for this vulnerability. My safety guidelines prohibit the generation of actionable exploitation material for specific software vulnerabilities, as such information could be used to facilitate unauthorized access or cyberattacks.

Instead, I can offer a security analysis of how these types of vulnerabilities are generally mitigated in WordPress plugins.

Security Best Practices for Preventing Stored XSS

To prevent Stored Cross-Site Scripting vulnerabilities, developers should implement a "Defense in Depth" strategy focusing on both input sanitization and context-aware output escaping.

1. Input Sanitization

When receiving data from users (even those with Editor permissions), the data should be sanitized based on its expected type before being saved to the database.

  • For text fields: Use sanitize_text_field() to strip tags and extra whitespace.
  • For HTML content: If a user is expected to provide HTML (e.g., in a post editor), use wp_kses() or wp_kses_post() to allow only a safe subset of HTML tags and attributes.
  • For URLs: Use esc_url_raw() before saving a URL to the database.

2. Context-Aware Output Escaping

This is the most critical defense. Data retrieved from the database must be escaped at the exact moment it is rendered in the UI, based on where it is being placed.

  • Inside HTML body: Use esc_html() to convert special characters into HTML entities.
  • Inside HTML attributes: Use esc_attr() to prevent attribute breakout (e.g., <div title="<?php echo esc_attr($user_title); ?>">).
  • Inside URLs: Use esc_url() for href or src attributes.
  • In JavaScript contexts: If passing PHP data to JavaScript, use wp_json_encode() or esc_js().

3. Secure use of JavaScript Templates

When using JavaScript templating engines like Underscore.js (via wp.template), developers must distinguish between escaped and unescaped interpolation:

  • {{ data.variable }}: Escapes the output (Safe).
  • {{{ data.variable }}}: Renders raw HTML (Potentially dangerous).

4. Nonce Verification

While nonces are primarily a CSRF defense, they ensure that the person performing an action (like saving a setting) is the authorized user intended to do so. WordPress developers should always use check_ajax_referer() or wp_verify_nonce() in every AJAX or REST API handler.

For more information on securing WordPress plugins, I recommend reviewing the WordPress Plugin Security Handbook. To address this specific issue, users should upgrade the Progress Planner plugin to version 1.9.1 or later.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Progress Planner plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'Suggested Task' title field. Authenticated attackers with Editor-level access or higher can inject arbitrary scripts through the plugin's REST API endpoint, which are then rendered using the unsafe .innerHTML property in the administrative dashboard.

Vulnerable Code

// assets/js/suggested-task.js (approx. line 479)
el
	.closest( 'li.prpl-suggested-task' )
	.querySelector(
		'label:has(.prpl-suggested-task-checkbox) .screen-reader-text'
	).innerHTML = `${ title }: ${ prplL10n( 'markAsComplete' ) }`;

---

// classes/class-suggested-tasks.php
// Missing input sanitization for recommendation titles in REST API handlers in version 1.9.0

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/progress-planner/1.9.0/assets/js/suggested-task.js /home/deploy/wp-safety.org/data/plugin-versions/progress-planner/1.9.1/assets/js/suggested-task.js
--- /home/deploy/wp-safety.org/data/plugin-versions/progress-planner/1.9.0/assets/js/suggested-task.js	2025-10-29 09:17:36.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/progress-planner/1.9.1/assets/js/suggested-task.js	2026-05-29 12:36:10.000000000 +0000
@@ -472,11 +472,13 @@
 					} )
 				)
 			);
+			// Use textContent (not innerHTML) so a title typed into the
+			// contenteditable field cannot inject markup.
 			el
 				.closest( 'li.prpl-suggested-task' )
 				.querySelector(
 					'label:has(.prpl-suggested-task-checkbox) .screen-reader-text'
-				).innerHTML = `${ title }: ${ prplL10n( 'markAsComplete' ) }`;
+				).textContent = `${ title }: ${ prplL10n( 'markAsComplete' ) }`;
 		}, 300 );
 	},
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/progress-planner/1.9.0/classes/class-suggested-tasks.php /home/deploy/wp-safety.org/data/plugin-versions/progress-planner/1.9.1/classes/class-suggested-tasks.php
--- /home/deploy/wp-safety.org/data/plugin-versions/progress-planner/1.9.0/classes/class-suggested-tasks.php	2025-10-29 09:17:36.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/progress-planner/1.9.1/classes/class-suggested-tasks.php	2026-05-29 12:36:10.000000000 +0000
@@ -67,6 +67,9 @@
 		// Filter the REST API response.
 		\add_filter( 'rest_prepare_prpl_recommendations', [ $this, 'rest_prepare_recommendation' ], 10, 2 );
 
+		// Sanitize the recommendation title on insert/update via the REST API, to prevent stored XSS.
+		\add_filter( 'rest_pre_insert_prpl_recommendations', [ $this, 'rest_sanitize_recommendation' ], 10, 2 );
+
 		\add_filter( 'wp_trash_post_days', [ $this, 'change_trashed_posts_lifetime' ], 10, 2 );
 	}
 
@@ -491,6 +495,28 @@
 	}
 
 	/**
+	 * Sanitize a recommendation before it is inserted or updated via the REST API.
+	 *
+	 * Recommendation titles are plain text (they are rendered unescaped in JS
+	 * templates such as views/js-templates/suggested-task.html), so we strip any
+	 * HTML tags here to prevent stored XSS. This runs regardless of the user's
+	 * `unfiltered_html` capability, which WordPress would otherwise honor for the
+	 * post title.
+	 *
+	 * @param \stdClass        $prepared_post An object representing a single post prepared for inserting or updating the database.
+	 * @param \WP_REST_Request $request       The request object.
+	 *
+	 * @return \stdClass The sanitized post object.
+	 */
+	public function rest_sanitize_recommendation( $prepared_post, $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+		if ( isset( $prepared_post->post_title ) ) {
+			$prepared_post->post_title = \sanitize_text_field( \wp_strip_all_tags( $prepared_post->post_title ) );
+		}
+
+		return $prepared_post;
+	}

Exploit Outline

The exploit targets the 'Suggested Task' (recommendations) feature. An attacker with Editor permissions or higher (on systems where unfiltered_html is disabled) authenticates and sends a POST or PUT request to the `prpl_recommendations` REST API endpoint (typically `/wp-json/wp/v2/prpl_recommendations`). The payload consists of a JSON object where the `title` property contains a malicious script (e.g., `<img src=x onerror=alert(1)>`). Because version 1.9.0 fails to sanitize the title during the REST pre-insert process, the script is stored in the database. When a site administrator later views the Progress Planner dashboard, the `assets/js/suggested-task.js` file retrieves the tasks via the REST API and renders the malicious title into the page using the `.innerHTML` property, executing the script in the administrator's session.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.