Advanced IP Blocker WordPress plugin banner

CVE-2026-42739: Advanced IP Blocker Unauthenticated Stored XSS (CVSS 7.2)

Updated 8 min read

CVE-2026-42739 is a CVSS 7.2 (High) Unauthenticated Stored Cross-Site Scripting vulnerability in the Advanced IP Blocker WordPress plugin. An unauthenticated attacker can send crafted HTTP requests to any WordPress site running the vulnerable plugin. The malicious payload is stored in the database. When an administrator views the Blocked Attack Signatures panel, the stored script executes in the admin’s browser.

Vulnerability Summary

FieldValue
Plugin NameAdvanced IP Blocker
Plugin Slugadvanced-ip-blocker
CVE IDCVE-2026-42739
CVSS Score7.2 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Vulnerability TypeUnauthenticated Stored Cross-Site Scripting
Affected Versions<= 8.10.7
Patched Version8.10.8
PublishedMay 28, 2026
ResearcherPeng Zhou
Wordfence AdvisoryLink

Description

The Advanced IP Blocker plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to and including 8.10.7. The vulnerability exists due to two combined weaknesses: raw HTTP request headers are stored in the database without sanitization, and the JavaScript modal that displays signature details renders those stored values directly into innerHTML without HTML escaping. This allows unauthenticated attackers to inject arbitrary web scripts that execute whenever an administrator views the Blocked Attack Signatures panel.

Technical Analysis

How the Plugin Logs Requests

The plugin hooks log_request_signature() on the WordPress init action at priority -2. This function runs for every visitor request. It calls fingerprint_manager->get_request_headers_for_log() to collect HTTP headers, then inserts the data into the advaipbl_request_log database table.

File: includes/class-advaipbl-fingerprint-manager.php, lines 41–57

public function get_request_headers_for_log() {
    $headers_to_log = [
        'Accept', 'Accept-Language', 'Accept-Encoding', 'Referer', 'Origin',
        'CF-Connecting-IP', 'X-Forwarded-For', 'X-Real-IP'
    ];

    $collected_headers = [];
    foreach ($_SERVER as $key => $value) {
        if (strpos($key, 'HTTP_') === 0) {
            $header = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
            if (in_array($header, $headers_to_log)) {
                $collected_headers[$header] = $value; // RAW value — no sanitization
            }
        }
    }
    return wp_json_encode($collected_headers);
}

The raw $_SERVER values are stored without sanitize_text_field() or any other escaping. Headers such as Accept-Language, Referer, and X-Forwarded-For can contain arbitrary attacker-controlled content.

How Fingerprints Are Computed

The fingerprint hash is computed in generate_signature() using four sanitized values: User-Agent, Accept, Accept-Language, and Accept-Encoding. Crucially, the hash computation uses sanitize_text_field() on these values, but get_request_headers_for_log() stores the raw header values separately.

File: includes/class-advaipbl-fingerprint-manager.php, lines 21–33

public function generate_signature() {
    $signature_parts = [];
    $signature_parts[] = $this->main_class->get_user_agent(); // sanitized
    $signature_parts[] = isset($_SERVER['HTTP_ACCEPT']) ?
        sanitize_text_field(wp_unslash($_SERVER['HTTP_ACCEPT'])) : 'no-accept';
    $signature_parts[] = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ?
        sanitize_text_field(wp_unslash($_SERVER['HTTP_ACCEPT_LANGUAGE'])) : 'no-language';
    $signature_parts[] = isset($_SERVER['HTTP_ACCEPT_ENCODING']) ?
        sanitize_text_field(wp_unslash($_SERVER['HTTP_ACCEPT_ENCODING'])) : 'no-encoding';

    $signature_string = implode('|', $signature_parts);
    return hash('sha256', $signature_string);
}

Because Accept-Language: en-US,<img src=x onerror=alert(1)> sanitizes to en-US, (HTML stripped), the resulting fingerprint hash is nearly identical to a normal Accept-Language: en-US request. The raw malicious header, however, is stored separately in the request_headers column.

Signature Flagging and Storage

When the Signature Analysis feature is enabled, the cron job execute_signature_analysis() calls analyze_and_flag_signatures(). This function queries advaipbl_request_log for signatures seen from more than N distinct IPs within a time window. When the threshold is met, the signature is inserted into advaipbl_malicious_signatures.

File: includes/class-advaipbl-fingerprint-manager.php, line 111–136

The function also retrieves a sample request (user agent and request URI) to store as metadata, and the get_signature_details() function later retrieves the full request_headers column of the first logged request for that signature hash.

public function get_signature_details($signature_hash) {
    global $wpdb;
    $log_table = $wpdb->prefix . 'advaipbl_request_log';

    $sample_request = $wpdb->get_row($wpdb->prepare(
        "SELECT user_agent, request_headers FROM {$log_table}
         WHERE signature_hash = %s LIMIT 1",
        $signature_hash
    ));

    $details = [
        'sample_user_agent' => $sample_request->user_agent,
        'sample_headers'    => json_decode($sample_request->request_headers, true) ?: [],
        'evidence'          => $evidence,
    ];

    return $details;
}

The sample_headers array contains the raw, unsanitized header values from the original request.

The Unescaped JavaScript Renderer

When an administrator clicks “View Details” on a flagged signature, the admin JavaScript makes an AJAX call to advaipbl_get_signature_details. The response is rendered directly into the modal’s innerHTML using JavaScript template literals.

File: js/admin-logs.js (vulnerable version 8.10.7), lines 222–241

let detailsHtml = '<h4>Signature Components:</h4><ul class="signature-components">';
// No escaping — raw value injected into innerHTML:
detailsHtml += `<li><strong>User-Agent:</strong> <code>${details.sample_user_agent || 'N/A'}</code></li>`;

if (details.sample_headers) {
    for (const [key, value] of Object.entries(details.sample_headers)) {
        // No escaping — raw key and value injected into innerHTML:
        detailsHtml += `<li><strong>${key}:</strong> <code>${value}</code></li>`;
    }
}

// Evidence table — request_uri also unescaped:
detailsHtml += `<tr>
  <td><code title="${ev.ip_hash}">${ipHashShort}</code></td>
  <td>${ev.request_uri}</td>
  <td>${timeAgo}</td>
  <td>${notesCell}</td>
</tr>`;

modal.find('.details-content').html(detailsHtml); // Rendered to DOM

Template literals directly interpolate details.sample_headers[value] and ev.request_uri into the HTML string. When this string is assigned to .html(), jQuery parses and executes any embedded JavaScript.

Why It Is Unauthenticated

The attacker does not log in. They simply send HTTP requests with malicious header values. Every request to the WordPress frontend triggers log_request_signature() at init priority -2, before any authentication check. The raw headers are stored in the database. The payload fires later when a privileged user views the admin panel.

Proof of Concept

Disclaimer: This proof of concept is provided for educational and authorized testing purposes only. Do not use this against systems you do not own or have explicit written permission to test.

Prerequisites:

  • Target runs WordPress with Advanced IP Blocker ≤ 8.10.7 installed and active
  • “Signature Analysis” is enabled in the plugin settings (Blocking Rules → Firewall)
  • The default IP threshold is 5 (5 distinct source IPs required to flag a signature)

Step 1 — Send malicious requests from 5 different IPs

Send the following request from 5 distinct source IP addresses (VPN, proxies, cloud instances). The Accept-Language header carries the XSS payload and is stored raw in the database.

# Replace TARGET-SITE with the vulnerable WordPress URL
# Run this from 5 different source IP addresses

curl -s 'https://TARGET-SITE/' \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)' \
  -H 'Accept: text/html,application/xhtml+xml' \
  -H 'Accept-Language: en-US,<img src=x onerror=alert(document.cookie)>' \
  -H 'Accept-Encoding: gzip, deflate'

Because sanitize_text_field('<img src=x onerror=alert(document.cookie)>') strips HTML tags, the signature hash for this request matches any request using the same User-Agent, Accept, and Accept-Encoding headers. The raw malicious Accept-Language value (en-US,<img src=x onerror=alert(document.cookie)>) is stored in the request_headers column without modification.

Step 2 — Wait for the signature analysis cron to run

By default, execute_signature_analysis() runs on the advaipbl_signature_analysis_event WP-Cron schedule. On a low-traffic site, you can trigger it by visiting the site with the doing_wp_cron query parameter, or wait for WordPress to run cron automatically on the next page load.

Once the cron runs, the signature (seen from 5+ distinct IPs) is flagged and inserted into advaipbl_malicious_signatures.

Step 3 — Trigger the XSS as an administrator

  1. Log into the WordPress admin panel
  2. Navigate to Advanced IP Blocker → Blocking Rules → Attack Signatures
  3. Locate the flagged signature in the table
  4. Click the “View Details” button

The modal opens. The JavaScript fetches signature details via AJAX. The Accept-Language header value (en-US,<img src=x onerror=alert(document.cookie)>) is injected directly into innerHTML. The browser parses the <img> tag, the onerror handler fires, and the alert executes.

Expected result: alert(document.cookie) executes in the administrator’s browser context, demonstrating full code execution. A real attacker would replace the payload with one that creates a rogue admin user or exfiltrates session cookies.

Patch Analysis

The fix in version 8.10.8 is entirely in js/admin-logs.js. The patch adds an escapeHtml() helper function that uses the browser’s built-in DOM text node mechanism to perform safe HTML escaping. This helper is then applied to every user-controlled value before interpolation.

Diff (8.10.7 → 8.10.8):

+                    const escapeHtml = (text) => {
+                        if (!text) return 'N/A';
+                        const div = document.createElement('div');
+                        div.textContent = text;
+                        return div.innerHTML;
+                    };
+
                     let detailsHtml = '<h4>Signature Components:</h4><ul class="signature-components">';
-                    detailsHtml += `<li><strong>User-Agent:</strong> <code>${details.sample_user_agent || 'N/A'}</code></li>`;
+                    detailsHtml += `<li><strong>User-Agent:</strong> <code>${escapeHtml(details.sample_user_agent)}</code></li>`;
 
                     if (details.sample_headers) {
                         for (const [key, value] of Object.entries(details.sample_headers)) {
-                            detailsHtml += `<li><strong>${key}:</strong> <code>${value}</code></li>`;
+                            detailsHtml += `<li><strong>${escapeHtml(key)}:</strong> <code>${escapeHtml(value)}</code></li>`;
                         }
                     }
                     // ...
-                    detailsHtml += `<tr><td><code title="${ev.ip_hash}">${ipHashShort}</code></td><td>${ev.request_uri}</td><td>${timeAgo}</td><td>${notesCell}</td></tr>`;
+                    detailsHtml += `<tr><td><code title="${escapeHtml(ev.ip_hash)}">${ipHashShort}</code></td><td>${escapeHtml(ev.request_uri)}</td><td>${escapeHtml(timeAgo)}</td><td>${notesCell}</td></tr>`;

The escapeHtml() function works by assigning the raw string to a DOM text node (div.textContent = text). The browser treats it as plain text and automatically escapes any HTML characters. Reading it back with div.innerHTML returns the safely escaped string. This approach is reliable, library-free, and correct for all HTML contexts.

The fix addresses all four injection points:

  • details.sample_user_agent (User-Agent from the database)
  • Header key and value (key, value from details.sample_headers)
  • Evidence table: ev.ip_hash, ev.request_uri, timeAgo

Note that the fix is client-side only. The server-side input (raw headers stored in request_headers) is not sanitized in the patch. The root cause on the server side — storing unsanitized header values — remains. A more complete fix would also sanitize the headers at storage time using sanitize_text_field().

Timeline

DateEvent
May 28, 2026Vulnerability publicly disclosed by Wordfence
May 28, 2026Patch released (version 8.10.8)
June 7, 2026This post published

Remediation

Update Advanced IP Blocker to version 8.10.8 or later from the WordPress admin dashboard (Plugins → Installed Plugins → Update) or directly from wordpress.org.

If you cannot update immediately, disable the Signature Analysis feature in Advanced IP Blocker → Settings to prevent signatures from being flagged and displayed until you can apply the patch.

References

  1. Wordfence Advisory — CVE-2026-42739
  2. Patchstack Advisory
  3. Plugin Changeset (8.10.7 → 8.10.8)
  4. CVE Record — CVE-2026-42739
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee