CVE-2026-42739: Advanced IP Blocker Unauthenticated Stored XSS (CVSS 7.2)
Table of Contents
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
| Field | Value |
|---|---|
| Plugin Name | Advanced IP Blocker |
| Plugin Slug | advanced-ip-blocker |
| CVE ID | CVE-2026-42739 |
| CVSS Score | 7.2 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N |
| Vulnerability Type | Unauthenticated Stored Cross-Site Scripting |
| Affected Versions | <= 8.10.7 |
| Patched Version | 8.10.8 |
| Published | May 28, 2026 |
| Researcher | Peng Zhou |
| Wordfence Advisory | Link |
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
- Log into the WordPress admin panel
- Navigate to Advanced IP Blocker → Blocking Rules → Attack Signatures
- Locate the flagged signature in the table
- 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,valuefromdetails.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
| Date | Event |
|---|---|
| May 28, 2026 | Vulnerability publicly disclosed by Wordfence |
| May 28, 2026 | Patch released (version 8.10.8) |
| June 7, 2026 | This 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.