CVE-2026-52702

SEO Redirection Plugin – 301 Redirect Manager <= 9.17 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
9.18
Patched in
4d
Time to patch

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.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=9.17
PublishedJune 12, 2026
Last updatedJune 15, 2026
Affected pluginseo-redirection

What Changed in the Fix

Changes introduced in v9.18

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This exploitation research plan targets **CVE-2026-52702**, an unauthenticated stored cross-site scripting (XSS) vulnerability in the **SEO Redirection Plugin – 301 Redirect Manager** (versions <= 9.17). ### 1. Vulnerability Summary The vulnerability arises from the plugin's feature that logs 404 e…

Show full research plan

This exploitation research plan targets CVE-2026-52702, an unauthenticated stored cross-site scripting (XSS) vulnerability in the SEO Redirection Plugin – 301 Redirect Manager (versions <= 9.17).

1. Vulnerability Summary

The vulnerability arises from the plugin's feature that logs 404 errors and redirection hits. When a visitor triggers a 404 error or a configured redirect, the plugin records metadata about the request—including the Request URL, Referer, and User-Agent—into the WordPress database.

Because the plugin fails to properly sanitize this input before storage or, more critically, fails to escape it when displaying the history logs in the admin dashboard (using the datagrid class), an unauthenticated attacker can inject malicious scripts into the logs. These scripts execute when an administrator views the "404 Errors" or "History" tabs.

2. Attack Vector Analysis

  • Endpoint: Any non-existent URL (to trigger a 404 log) or a URL that matches an existing redirection rule.
  • Vulnerable Parameters: HTTP_REFERER header, HTTP_USER_AGENT header, or the REQUEST_URI (specifically query parameters).
  • Authentication: None (Unauthenticated).
  • Preconditions: The plugin must have 404 logging enabled (this is a core feature and typically enabled by default).

3. Code Flow

  1. Entry Point: A visitor requests a URL that triggers WPSR_redirect (hooked to wp in seo-redirection.php).
  2. Detection: If the request results in a 404, the plugin calls a logging function (e.g., WPSR_add_404_log).
  3. Data Collection: The logging function retrieves request data. In common/util.php, the get_current_URL() and get_ref() functions are used to fetch the URL and Referer.
    • Bypass Note: util.php shows urldecode(sanitize_text_field($_SERVER['QUERY_STRING'])) in WPSR_get_current_parameters. Applying urldecode after sanitization effectively re-introduces malicious characters like < and >.
  4. Storage: The unsanitized/decoded string is inserted into the 404 history table (e.g., wp_WP_SEO_404_History).
  5. Sink (Admin View): An administrator navigates to the plugin settings: wp-admin/options-general.php?page=seo-redirection.php&tab=404.
  6. Rendering: The page uses the datagrid class (common/controls/datagrid.class.php). The grid calls add_data_col or add_html_col to display the log entries. If these columns are rendered without esc_html(), the stored script executes.

4. Nonce Acquisition Strategy

No nonce is required for the injection phase because the logging mechanism is triggered by unauthenticated frontend requests.

To verify the exploit as an admin:

  1. The agent must log in as an administrator.
  2. Navigate to the 404 log tab: admin_url('options-general.php?page=seo-redirection.php&tab=404').
  3. No nonce is needed to view the logs, though one may be needed if the exploit requires interacting with the "Delete" or "Redirect" buttons in the grid.

5. Exploitation Strategy

The goal is to trigger a 404 error while providing a malicious payload in the Referer or URL.

Step 1: Trigger 404 via Malicious Referer

  • Method: GET
  • URL: http://[target]/this-page-does-not-exist-123
  • Headers:
    • Referer: <script>alert('XSS_REFERER')</script>
    • User-Agent: <img src=x onerror=alert('XSS_UA')>

Step 2: Trigger 404 via Malicious Query String

  • Method: GET
  • URL: http://[target]/trigger-404?debug=%3Cscript%3Ealert('XSS_URL')%3C/script%3E
  • Note: Use URL encoding to ensure the payload survives the initial HTTP request.

Step 3: Admin Trigger

  • Use browser_navigate to visit the 404 History tab.
  • Monitor for dialogs/alerts using browser_eval.

6. Test Data Setup

  1. Ensure the plugin SEO Redirection (v9.17) is active.
  2. Ensure 404 logging is enabled in the plugin settings (usually default).
  3. Create a "Canary" 404 request to confirm logging is working:
    • GET /canary-404
    • Verify entry appears in wp-admin/options-general.php?page=seo-redirection.php&tab=404.

7. Expected Results

  • The malicious payload is stored in the database table tracking 404 errors.
  • When an administrator views the 404 History tab, the browser executes the injected JavaScript.
  • The datagrid should render the payload as raw HTML instead of literal text.

8. Verification Steps

After sending the malicious HTTP requests, verify the storage in the database via WP-CLI:

# Check if the payload exists in the 404 history table
wp db query "SELECT * FROM $(wp db prefix)WP_SEO_404_History ORDER BY id DESC LIMIT 5;"

Verify the output rendering:

  1. Navigate to the admin page.
  2. Inspect the HTML source of the table: browser_eval("document.querySelector('.grid').innerHTML").
  3. Look for the unescaped <script> or <img> tags.

9. Alternative Approaches

  • Redirection Hit Vector: If the site has an existing redirect (e.g., /old -> /new), request the "from" URL with a malicious Referer. The plugin might log successful redirects in a "History" tab (tab=history).
  • Admin Bar Vector: The plugin displays the latest 404 in the Admin Bar for logged-in users (see WPSR_add_link_to_admin_bar). If the relative_url is reflected there without escaping, XSS will trigger on every admin page load, not just the settings page.
    • Code Reference: seo-redirection.php lines 135-155 uses $relative_url in admin_url(...) and node titles. Check if add_node titles are escaped.
Research Findings
Static analysis — not yet PoC-verified

Summary

The SEO Redirection Plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting (XSS) due to insufficient sanitization and escaping of request metadata, such as the Referer header, User-Agent, and visitor IP address. Attackers can inject malicious scripts by triggering 404 errors or redirection events, which are then executed in the context of an administrator viewing the plugin's log dashboard.

Vulnerable Code

/* common/util.php line 352 */
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) ;
}

---

/* common/util.php line 445 */
public function WPSR_get_current_parameters($remove_parameter="")
{	
	
	if($_SERVER['QUERY_STRING']!='')
	{
		$qry = '?' . urldecode(sanitize_text_field($_SERVER['QUERY_STRING']));

---

/* common/controls/datagrid.class.php around line 442 (inferred from patch) */
if (is_array($ar)) {
    foreach ($ar as $key => $value) {
        $key_var = "db_" . $key;
        $html = str_ireplace('{' . $key_var . '}', ${$key_var} ?? '', $html);
    }
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/seo-redirection/9.17/common/controls/datagrid.class.php /home/deploy/wp-safety.org/data/plugin-versions/seo-redirection/9.18/common/controls/datagrid.class.php
--- /home/deploy/wp-safety.org/data/plugin-versions/seo-redirection/9.17/common/controls/datagrid.class.php	2026-04-14 14:33:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/seo-redirection/9.18/common/controls/datagrid.class.php	2026-06-06 11:23:48.000000000 +0000
@@ -442,7 +442,10 @@
 					if (is_array($ar)) {
 						foreach ($ar as $key => $value) {
 							$key_var = "db_" . $key;
-							$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);
 						}
 					}
 					$row[$i] = $html;
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/seo-redirection/9.17/common/util.php /home/deploy/wp-safety.org/data/plugin-versions/seo-redirection/9.18/common/util.php
--- /home/deploy/wp-safety.org/data/plugin-versions/seo-redirection/9.17/common/util.php	2026-04-14 14:33:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/seo-redirection/9.18/common/util.php	2026-06-06 11:23:48.000000000 +0000
@@ -352,15 +352,25 @@
 
 public function get_visitor_IP()
 {
-	$ipaddress = sanitize_text_field($_SERVER['REMOTE_ADDR']);
+	$ipaddress = isset($_SERVER['REMOTE_ADDR']) ? 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']);
+		// 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;
 }

Exploit Outline

1. Identify an endpoint on the WordPress site that triggers a 404 error (any non-existent URL). 2. Craft an HTTP request to this endpoint containing an XSS payload (e.g., `<script>alert(document.cookie)</script>`) in headers like `Referer` or `User-Agent`. 3. Alternatively, supply a malicious payload in the query string of the URL; the plugin's `WPSR_get_current_parameters` function decodes the query string after sanitization, potentially re-introducing malicious characters. 4. If the server trusts proxy headers, the `X-Forwarded-For` header can also be used to inject a payload. 5. Wait for an administrator to log into the WordPress dashboard and navigate to 'Settings' > 'SEO Redirection' > '404 Errors' or 'History'. 6. The payload executes when the `datagrid` class renders the log table, as it replaces placeholders with raw database values without output escaping.

Check if your site is affected.

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