CVE-2026-3239

Strong Testimonials <= 3.2.21 - Authenticated (Contributor+) Stored Cross-Site Scripting via testimonial_view Shortcode

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

Description

The Strong Testimonials plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the plugin's testimonial_view shortcode in all versions up to, and including, 3.2.21 due to insufficient input sanitization and output escaping on user supplied attributes. 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.2.21
PublishedApril 7, 2026
Last updatedApril 8, 2026
Affected pluginstrong-testimonials

What Changed in the Fix

Changes introduced in v3.2.22

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

## Vulnerability Summary The **Strong Testimonials** plugin (versions <= 3.2.21) is vulnerable to **Stored Cross-Site Scripting (XSS)** via the `testimonial_view` shortcode. The vulnerability exists because the plugin fails to sanitize or escape attributes provided by the user in the shortcode befor…

Show full research plan

Vulnerability Summary

The Strong Testimonials plugin (versions <= 3.2.21) is vulnerable to Stored Cross-Site Scripting (XSS) via the testimonial_view shortcode. The vulnerability exists because the plugin fails to sanitize or escape attributes provided by the user in the shortcode before rendering them in the HTML output or passing them to JavaScript localization functions. Specifically, attributes such as id or view are used to construct HTML elements and JavaScript variable names.

Attack Vector Analysis

  • Shortcode: [testimonial_view]
  • Vulnerable Attribute: id (and potentially view, class, or title depending on the template).
  • Authentication Level: Authenticated (Contributor or higher). Contributors can create posts and insert shortcodes.
  • Preconditions: The plugin must be active. A "View" does not necessarily need to exist for the attribute processing to trigger the vulnerability, but having one ensures the full rendering cycle.
  • Payload Location: The payload is stored in the post_content of a WordPress post/page.

Code Flow

  1. Entry Point: A user with edit_posts capability (Contributor+) creates a post containing the shortcode: [testimonial_view id='<payload>'].
  2. Processing: When the page is viewed, WordPress parses the shortcode. The Strong_Testimonials_Render class processes the attributes.
  3. Attribute Storage: In includes/class-strong-testimonials-render.php, the set_atts($atts) method stores the user-supplied attributes in $this->view_atts.
  4. Rendering Trigger: The rendering process calls WPMST()->render->prerender( $atts ) and WPMST()->render->view_rendered().
  5. Vulnerable Sink 1 (JS Variable Name): The view_rendered() method calls view_rendered_after(), which executes `localize
Research Findings
Static analysis — not yet PoC-verified

Summary

The Strong Testimonials plugin is vulnerable to Stored Cross-Site Scripting via the 'id' attribute of the [testimonial_view] shortcode. Authenticated attackers with Contributor-level access can inject malicious payloads that break out of unquoted HTML data attributes, leading to arbitrary JavaScript execution in the context of a victim's browser.

Vulnerable Code

// includes/class-strong-testimonials-render.php @ line 647
public function parse_view( $out, $pairs, $atts ) {
		// Convert "id" to "view"
		if ( isset( $atts['id'] ) && $atts['id'] ) {
			$atts['view'] = $atts['id'];
			unset( $atts['id'] );
		} else {

---

// includes/functions-template.php @ line 551
function wpmtst_container_data() {
	$data_array = apply_filters( 'wpmtst_container_data', WPMST()->atts( 'container_data' ) );
	if ( $data_array ) {
		$data = '';
		foreach ( $data_array as $attr => $value ) {
			$data .= " data-$attr=$value";
		}
		echo esc_attr( $data );
	}
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/strong-testimonials/3.2.21/includes/class-strong-testimonials-render.php /home/deploy/wp-safety.org/data/plugin-versions/strong-testimonials/3.2.22/includes/class-strong-testimonials-render.php
--- /home/deploy/wp-safety.org/data/plugin-versions/strong-testimonials/3.2.21/includes/class-strong-testimonials-render.php	2024-09-10 11:56:04.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/strong-testimonials/3.2.22/includes/class-strong-testimonials-render.php	2026-02-26 10:04:58.000000000 +0000
@@ -647,9 +647,15 @@
 	 * @return array
 	 */
 	public function parse_view( $out, $pairs, $atts ) {
-		// Convert "id" to "view"
-		if ( isset( $atts['id'] ) && $atts['id'] ) {
-			$atts['view'] = $atts['id'];
+		// Convert "id" to "view" - sanitize to integer to prevent attribute breakout (security).
+		if ( isset( $atts['id'] ) && $atts['id'] !== '' ) {
+			$raw_id  = trim( (string) $atts['id'] );
+			$view_id = absint( $raw_id );
+			// Reject non-numeric or malformed id (e.g. "1 onmouseover=alert(1)").
+			if ( $view_id < 1 || $raw_id !== (string) $view_id ) {
+				return array_merge( array( 'view_not_found' => 1 ), $atts );
+			}
+			$atts['view'] = $view_id;
 			unset( $atts['id'] );
 		} else {
 			return array_merge( array( 'view_not_found' => 1 ), $atts );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/strong-testimonials/3.2.21/includes/functions-template.php /home/deploy/wp-safety.org/data/plugin-versions/strong-testimonials/3.2.22/includes/functions-template.php
--- /home/deploy/wp-safety.org/data/plugin-versions/strong-testimonials/3.2.21/includes/functions-template.php	2026-01-13 10:10:46.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/strong-testimonials/3.2.22/includes/functions-template.php	2026-02-26 10:04:58.000000000 +0000
@@ -551,12 +551,15 @@
 
 function wpmtst_container_data() {
 	$data_array = apply_filters( 'wpmtst_container_data', WPMST()->atts( 'container_data' ) );
-	if ( $data_array ) {
-		$data = '';
+	if ( $data_array && is_array( $data_array ) ) {
+		$parts = array();
 		foreach ( $data_array as $attr => $value ) {
-			$data .= " data-$attr=$value";
+			$attr    = sanitize_key( $attr );
+			$value   = esc_attr( (string) $value );
+			$parts[] = sprintf( ' data-%s="%s"', $attr, $value );
 		}
-		echo esc_attr( $data );
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Each part built from sanitize_key and esc_attr.
+		echo implode( '', $parts );
 	}
 }

Exploit Outline

1. An authenticated attacker with Contributor privileges (or higher) creates or edits a WordPress post. 2. The attacker inserts a `[testimonial_view]` shortcode with a malicious `id` attribute, such as: `[testimonial_view id='1 onmouseover=alert(1)']`. 3. The plugin processes this shortcode via the `Strong_Testimonials_Render` class. The `parse_view` method assigns the malicious payload to the `view` attribute without sanitization. 4. When the page is rendered, the `wpmtst_container_data` function builds the container's HTML attributes. Because the plugin does not quote the attribute values (e.g., `data-view=1 onmouseover=alert(1)`), the space in the payload allows the attacker to inject a new HTML attribute (`onmouseover`). 5. When any user, including an administrator, views the post and interacts with the testimonial container (e.g., hovers their mouse), the injected JavaScript executes.

Check if your site is affected.

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