Progress Planner <= 1.9.0 - Authenticated (Editor+) Stored Cross-Site Scripting
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:NTechnical Details
<=1.9.0What Changed in the Fix
Changes introduced in v1.9.1
Source Code
WordPress.org SVNI 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()orwp_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()forhreforsrcattributes. - In JavaScript contexts: If passing PHP data to JavaScript, use
wp_json_encode()oresc_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.
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
@@ -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 ); }, @@ -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.