VYPR
High severity7.1NVD Advisory· Published Jun 1, 2026· Updated Jun 1, 2026

CVE-2026-48839

CVE-2026-48839

Description

A DOM-based cross-site scripting (XSS) vulnerability in the WP Statistics plugin for WordPress allows attackers to inject malicious scripts via improper input neutralization.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A DOM-based cross-site scripting (XSS) vulnerability in the WP Statistics plugin for WordPress allows attackers to inject malicious scripts via improper input neutralization.

Vulnerability

The WP Statistics plugin for WordPress is susceptible to a DOM-based cross-site scripting (XSS) vulnerability due to improper neutralization of input during web page generation. This flaw affects all versions of the plugin from n/a through 14.16.6 [2]. The vulnerability exists within the plugin's handling of user-supplied data, allowing for the injection of arbitrary HTML or script payloads into the application's DOM.

Exploitation

Successful exploitation of this vulnerability requires user interaction, such as a privileged user clicking a malicious link, visiting a crafted page, or submitting a form [2]. Once the user interacts with the malicious content, the injected script is executed within the context of the victim's browser session, enabling the attacker to perform unauthorized actions on behalf of the user.

Impact

An attacker can leverage this XSS vulnerability to inject malicious scripts, including redirects, advertisements, and other HTML payloads [2]. This can lead to the compromise of user sessions, unauthorized data access, or the display of deceptive content to site visitors, potentially impacting the integrity and security of the affected WordPress site.

Mitigation

Users should update the WP Statistics plugin to version 14.16.7 or later to resolve this vulnerability [2]. If an immediate update is not possible, site administrators are advised to consult with their hosting provider or a web developer to implement security measures, such as web application firewall rules, to block potential exploit attempts.

AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

2
fae2d8be9761

Harden output escaping and input sanitization in device reports

https://github.com/wp-statistics/wp-statisticsmostafaApr 18, 2026Fixed in 14.16.7via llm-release-walk
9 files changed · +212 18
  • CHANGELOG.md+3 0 modified
    @@ -1,3 +1,6 @@
    +14.16.7 - 2026-04-18
    +- **Enhancement:** Hardened output escaping and input sanitization across device reports.
    +
     14.16.6 - 2026-04-16
     - **Fix:** Removed legacy TinyMCE integration that caused "Failed to load plugin" errors in the classic editor, especially with themes like Corvix and Avada.
     - **Fix:** Excluded browser prefetch and prerender requests that were inflating visit counts.
    
  • includes/admin/templates/pages/devices/browsers.php+3 3 modified
    @@ -36,9 +36,9 @@
                                     <?php foreach ($data['visitors'] as $item) : ?>
                                         <tr>
                                             <td class="wps-pd-l">
    -                                                <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?>" class="wps-browser-name">
    -                                                    <img alt="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?>" src="<?php echo esc_url(DeviceHelper::getBrowserLogo($item->agent)); ?>" class="log-tools wps-flag"/>
    -                                                    <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?>
    +                                                <span title="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent)); ?>" class="wps-browser-name">
    +                                                    <img alt="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent)); ?>" src="<?php echo esc_url(DeviceHelper::getBrowserLogo($item->agent)); ?>" class="log-tools wps-flag"/>
    +                                                    <?php echo esc_html(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent)); ?>
                                                     </span>
                                             </td>
                                             <td class="wps-pd-l">
    
  • includes/admin/templates/pages/devices/categories.php+2 2 modified
    @@ -32,8 +32,8 @@
                                     <?php foreach ($data['visitors'] as $item) : ?>
                                         <tr>
                                             <td class="wps-pd-l">
    -                                                <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->device); ?>" class="wps-model-name">
    -                                                    <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->device); ?>
    +                                                <span title="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->device)); ?>" class="wps-model-name">
    +                                                    <?php echo esc_html(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->device)); ?>
                                                     </span>
                                             </td>
                                             <td class="wps-pd-l">
    
  • includes/admin/templates/pages/devices/platforms.php+3 3 modified
    @@ -33,9 +33,9 @@
                                     <?php foreach ($data['visitors'] as $item) : ?>
                                         <tr>
                                             <td class="wps-pd-l">
    -                                                <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?>" class="wps-platform-name">
    -                                                    <img alt="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?>" src="<?php echo esc_url(DeviceHelper::getPlatformLogo($item->platform)); ?>" class="log-tools wps-flag"/>
    -                                                    <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?>
    +                                                <span title="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform)); ?>" class="wps-platform-name">
    +                                                    <img alt="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform)); ?>" src="<?php echo esc_url(DeviceHelper::getPlatformLogo($item->platform)); ?>" class="log-tools wps-flag"/>
    +                                                    <?php echo esc_html(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform)); ?>
                                                     </span>
                                             </td>
                                             <td class="wps-pd-l">
    
  • readme.txt+4 1 modified
    @@ -4,7 +4,7 @@ Donate link: https://wp-statistics.com/donate/
     Tags: analytics, google analytics, insights, stats, site visitors
     Requires at least: 6.6
     Tested up to: 7.0
    -Stable tag: 14.16.6
    +Stable tag: 14.16.7
     Requires PHP: 7.4
     License: GPL-2.0+
     License URI: https://www.gnu.org/licenses/gpl-2.0.html
    @@ -146,6 +146,9 @@ To ensure the plugin works correctly, please clear your cache because some reque
     Update add-ons DataPlus, Advanced Reporting, and Mini-Chart to the latest version.
     
     == Changelog ==
    += 14.16.7 - 2026-04-18 =
    +- **Enhancement:** Hardened output escaping and input sanitization across device reports.
    +
     = 14.16.6 - 2026-04-16 =
     - **Fix:** Removed legacy TinyMCE integration that caused "Failed to load plugin" errors in the classic editor, especially with themes like Corvix and Avada.
     - **Fix:** Excluded browser prefetch and prerender requests that were inflating visit counts.
    
  • src/Service/Analytics/DeviceDetection/UserAgentService.php+23 5 modified
    @@ -46,7 +46,7 @@ public function getDeviceDetector()
          */
         public function getBrowser()
         {
    -        return $this->deviceDetector ? $this->deviceDetector->getClient('name') : null;
    +        return $this->deviceDetector ? self::sanitizeDetectorValue($this->deviceDetector->getClient('name')) : null;
         }
     
         /**
    @@ -56,7 +56,7 @@ public function getBrowser()
          */
         public function getPlatform()
         {
    -        return $this->deviceDetector ? $this->deviceDetector->getOs('name') : null;
    +        return $this->deviceDetector ? self::sanitizeDetectorValue($this->deviceDetector->getOs('name')) : null;
         }
     
         /**
    @@ -76,7 +76,7 @@ public function getVersion()
          */
         public function getDevice()
         {
    -        return $this->deviceDetector ? $this->deviceDetector->getDeviceName() : null;
    +        return $this->deviceDetector ? self::sanitizeDetectorValue($this->deviceDetector->getDeviceName()) : null;
         }
     
         /**
    @@ -102,9 +102,27 @@ public function getModel()
                 }
     
                 $model = trim($brand . ' ' . $device);
    -        }      
    +        }
    +
    +        return self::sanitizeDetectorValue($model);
    +    }
    +
    +    /**
    +     * DeviceDetector's fallback regexes can capture raw bytes from the
    +     * User-Agent header, so restrict stored values to a safe subset before
    +     * they reach DB storage, admin reports, REST, or exports.
    +     */
    +    public static function sanitizeDetectorValue($value)
    +    {
    +        if ($value === null || $value === '') {
    +            return $value;
    +        }
    +
    +        $value = wp_strip_all_tags((string) $value);
    +        $value = preg_replace('/[^\p{L}\p{N}\s._\-\/+()&\']/u', '', $value);
    +        $value = trim(preg_replace('/\s+/', ' ', (string) $value));
     
    -        return $model ?? null;
    +        return $value;
         }
     
         /**
    
  • tests/integration/Test_UserAgentService.php+170 0 modified
    @@ -141,4 +141,174 @@ public function test_handles_missing_user_agent()
     
             $this->assertEquals('UNK', $userAgentService->getBrowser());
         }
    +
    +    public function test_sanitize_preserves_null_and_empty()
    +    {
    +        $this->assertNull(UserAgentService::sanitizeDetectorValue(null));
    +        $this->assertSame('', UserAgentService::sanitizeDetectorValue(''));
    +    }
    +
    +    public function test_sanitize_keeps_common_browser_names()
    +    {
    +        $this->assertSame('Chrome', UserAgentService::sanitizeDetectorValue('Chrome'));
    +        $this->assertSame('Microsoft Edge', UserAgentService::sanitizeDetectorValue('Microsoft Edge'));
    +        $this->assertSame('Samsung Browser', UserAgentService::sanitizeDetectorValue('Samsung Browser'));
    +    }
    +
    +    public function test_sanitize_keeps_allowed_punctuation()
    +    {
    +        $this->assertSame('Opera Mini/Beta', UserAgentService::sanitizeDetectorValue('Opera Mini/Beta'));
    +        $this->assertSame('UC Browser (HD)', UserAgentService::sanitizeDetectorValue('UC Browser (HD)'));
    +        $this->assertSame('Chrome-Mobile_v2.1', UserAgentService::sanitizeDetectorValue('Chrome-Mobile_v2.1'));
    +    }
    +
    +    public function test_sanitize_keeps_unicode_letters()
    +    {
    +        $this->assertSame('Yandex Браузер', UserAgentService::sanitizeDetectorValue('Yandex Браузер'));
    +    }
    +
    +    public function test_sanitize_strips_html_tags()
    +    {
    +        $this->assertSame('Chrome', UserAgentService::sanitizeDetectorValue('<script>alert(1)</script>Chrome'));
    +        $this->assertSame('Firefox', UserAgentService::sanitizeDetectorValue('<img src=x onerror=alert(1)>Firefox'));
    +    }
    +
    +    public function test_sanitize_neutralizes_attribute_breakers()
    +    {
    +        $clean = UserAgentService::sanitizeDetectorValue('evil" onload="alert(document.domain)');
    +
    +        $this->assertStringNotContainsString('"', $clean);
    +        $this->assertStringNotContainsString('<', $clean);
    +        $this->assertStringNotContainsString('>', $clean);
    +        $this->assertStringNotContainsString('=', $clean);
    +
    +        $this->assertSame("abc'", UserAgentService::sanitizeDetectorValue("abc'\"<>`"));
    +        $this->assertSame('abc1', UserAgentService::sanitizeDetectorValue("abc=1;"));
    +    }
    +
    +    public function test_sanitize_strips_control_characters_and_collapses_whitespace()
    +    {
    +        $this->assertSame('Chrome', UserAgentService::sanitizeDetectorValue("Chrome\x00\x01\x1f"));
    +        $this->assertSame('Chrome Mobile', UserAgentService::sanitizeDetectorValue("  Chrome    Mobile  "));
    +        $this->assertSame('Chrome Mobile', UserAgentService::sanitizeDetectorValue("Chrome\n\nMobile"));
    +    }
    +
    +    /**
    +     * End-to-end: the exact payload from the reported vulnerability must
    +     * not produce a stored browser name containing attribute-breaking
    +     * characters, so admin templates can't render an injected handler.
    +     */
    +    public function test_crafted_user_agent_is_sanitized_before_storage()
    +    {
    +        $_SERVER['HTTP_USER_AGENT'] = 'evil" onload="alert(document.domain)/1.2 (iPhone; iOS 16.0; Scale/3.00)';
    +
    +        $userAgentService = new UserAgentService();
    +        $browser          = (string) $userAgentService->getBrowser();
    +
    +        $this->assertStringNotContainsString('"', $browser);
    +        $this->assertStringNotContainsString('<', $browser);
    +        $this->assertStringNotContainsString('>', $browser);
    +        $this->assertStringNotContainsString('=', $browser);
    +    }
    +
    +    /**
    +     * Sanitizer must be a no-op for the browser/OS names that show up in
    +     * real traffic — regression guard so future tightening doesn't quietly
    +     * mangle mainstream labels.
    +     *
    +     * @dataProvider realWorldUserAgentProvider
    +     */
    +    public function test_sanitize_is_noop_for_real_user_agents($userAgent, $expectedBrowser, $expectedPlatform)
    +    {
    +        $_SERVER['HTTP_USER_AGENT'] = $userAgent;
    +
    +        $service = new UserAgentService();
    +
    +        $this->assertSame($expectedBrowser, $service->getBrowser());
    +        $this->assertSame($expectedPlatform, $service->getPlatform());
    +    }
    +
    +    public function realWorldUserAgentProvider()
    +    {
    +        return [
    +            'Chrome on Windows' => [
    +                'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    +                'Chrome',
    +                'Windows',
    +            ],
    +            'Firefox on macOS' => [
    +                'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0',
    +                'Firefox',
    +                'Mac',
    +            ],
    +            'Safari on iPhone' => [
    +                'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
    +                'Mobile Safari',
    +                'iOS',
    +            ],
    +            'Edge on Windows' => [
    +                'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
    +                'Microsoft Edge',
    +                'Windows',
    +            ],
    +            'Chrome on Android' => [
    +                'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
    +                'Chrome Mobile',
    +                'Android',
    +            ],
    +            'Samsung Internet' => [
    +                'Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36',
    +                'Samsung Browser',
    +                'Android',
    +            ],
    +            'Opera on Windows' => [
    +                'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 OPR/102.0.0.0',
    +                'Opera',
    +                'Windows',
    +            ],
    +            'Yandex on Android' => [
    +                'Mozilla/5.0 (Linux; arm_64; Android 11; SM-A515F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 YaBrowser/22.11.5.93.00 SA/3 Mobile Safari/537.36',
    +                'Yandex Browser',
    +                'Android',
    +            ],
    +            'IE 11' => [
    +                'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko',
    +                'Internet Explorer',
    +                'Windows',
    +            ],
    +        ];
    +    }
    +
    +    /**
    +     * Brand/OS labels that contain `&` or `'` must pass through unchanged
    +     * so stats aren't split across multiple buckets (e.g. AT&T vs ATT).
    +     * Both characters are safe inside double-quoted HTML attributes, and
    +     * the output sinks already apply esc_attr/esc_html.
    +     */
    +    public function test_sanitize_preserves_ampersand_and_apostrophe_in_labels()
    +    {
    +        $this->assertSame('AT&T', UserAgentService::sanitizeDetectorValue('AT&T'));
    +        $this->assertSame('Barnes & Noble', UserAgentService::sanitizeDetectorValue('Barnes & Noble'));
    +        $this->assertSame("BYJU'S", UserAgentService::sanitizeDetectorValue("BYJU'S"));
    +        $this->assertSame('Krüger&Matz', UserAgentService::sanitizeDetectorValue('Krüger&Matz'));
    +    }
    +
    +    /**
    +     * Extremely niche labels with `!` / `^` still get normalized. Pinned
    +     * here so we notice if the character class ever changes.
    +     */
    +    public function test_sanitize_normalizes_exclamation_and_caret()
    +    {
    +        $this->assertSame('Yahoo Japan Browser', UserAgentService::sanitizeDetectorValue('Yahoo! Japan Browser'));
    +        $this->assertSame('FRITZOS', UserAgentService::sanitizeDetectorValue('FRITZ!OS'));
    +        $this->assertSame('Symbian3', UserAgentService::sanitizeDetectorValue('Symbian^3'));
    +    }
    +
    +    public function test_sanitize_preserves_unicode_letters_in_labels()
    +    {
    +        $this->assertSame('Caixa Mágica', UserAgentService::sanitizeDetectorValue('Caixa Mágica'));
    +        $this->assertSame('Arçelik', UserAgentService::sanitizeDetectorValue('Arçelik'));
    +        $this->assertSame('ALDI SÜD', UserAgentService::sanitizeDetectorValue('ALDI SÜD'));
    +        $this->assertSame('Türk Telekom', UserAgentService::sanitizeDetectorValue('Türk Telekom'));
    +    }
     }
    
  • views/components/session-details.php+2 2 modified
    @@ -83,14 +83,14 @@
         <div class="wps-visitor__visitors-detail--row">
             <span><?php esc_html_e('City', 'wp-statistics'); ?></span>
             <div class="wps-ellipsis-parent">
    -            <span title="<?php echo Admin_Template::unknownToNotSet($visitor->getLocation()->getCity()) ?>"><?php echo Admin_Template::unknownToNotSet($visitor->getLocation()->getCity()) ?></span>
    +            <span title="<?php echo esc_attr(Admin_Template::unknownToNotSet($visitor->getLocation()->getCity())) ?>"><?php echo esc_html(Admin_Template::unknownToNotSet($visitor->getLocation()->getCity())) ?></span>
             </div>
         </div>
     
         <div class="wps-visitor__visitors-detail--row">
             <span><?php esc_html_e('Region', 'wp-statistics'); ?></span>
             <div class="wps-ellipsis-parent">
    -            <span title="<?php echo Admin_Template::unknownToNotSet($visitor->getLocation()->getRegion()) ?>"><?php echo Admin_Template::unknownToNotSet($visitor->getLocation()->getRegion()) ?></span>
    +            <span title="<?php echo esc_attr(Admin_Template::unknownToNotSet($visitor->getLocation()->getRegion())) ?>"><?php echo esc_html(Admin_Template::unknownToNotSet($visitor->getLocation()->getRegion())) ?></span>
             </div>
         </div>
     
    
  • wp-statistics.php+2 2 modified
    @@ -4,7 +4,7 @@
      * Plugin URI: https://wp-statistics.com/
      * GitHub Plugin URI: https://github.com/wp-statistics/wp-statistics
      * Description: Get website traffic insights with GDPR/CCPA compliant, privacy-friendly analytics. Includes visitor data, stunning graphs, and no data sharing.
    - * Version: 14.16.6
    + * Version: 14.16.7
      * Author: VeronaLabs
      * Author URI: https://veronalabs.com/
      * Text Domain: wp-statistics
    @@ -22,7 +22,7 @@
     require_once __DIR__ . '/includes/defines.php';
     
     # Set another useful plugin define.
    -define('WP_STATISTICS_VERSION', '14.16.6');
    +define('WP_STATISTICS_VERSION', '14.16.7');
     
     # Load Plugin
     if (!class_exists('WP_Statistics')) {
    
v14.16.7

Release: wp-statistics 14.16.7 (next version after vulnerable 14.16.6)

https://plugins.svn.wordpress.org/wp-statisticsFixed in 14.16.7via wp-release-tag

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

1

News mentions

0

No linked articles in our index yet.