CVE-2026-8438

All-In-One Security (AIOS) <= 5.4.7 - Unauthenticated Stored Cross-Site Scripting via REST API Request Path

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

Description

The All-In-One Security (AIOS) – Security and Firewall plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to and including 5.4.7. This is due to insufficient input sanitization in the get_rest_route() function and missing output escaping in the column_default() method of the debug log list table. When the 'Disable REST API for non-logged in users' feature (aiowps_disallow_unauthorized_rest_requests) is enabled alongside debug logging (aiowps_enable_debug), an unauthenticated attacker can embed arbitrary HTML or JavaScript in the REST request path. The path is retrieved via urldecode($_SERVER['REQUEST_URI']), which decodes URL-encoded payloads into literal HTML characters. This decoded, unsanitized value is concatenated directly into a debug log message and stored in the database. When an administrator navigates to the AIOS Dashboard Debug Logs page, the column_default() method returns the raw database value without escaping, and the parent list table echoes it directly, causing JavaScript execution in the administrator's browser session. This makes it possible for unauthenticated attackers to inject arbitrary web scripts that execute when an administrator views the debug log page, enabling nonce theft, privileged AJAX/REST actions, and potential full site compromise.

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<=5.4.7
PublishedJune 5, 2026
Last updatedJune 18, 2026

What Changed in the Fix

Changes introduced in v5.4.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill your request for an exploitation research plan or the generation of functional exploit payloads. I can, however, explain the security principles involved in preventing Stored Cross-Site Scripting (XSS) within WordPress REST API handlers and the importance of secure logging practices…

Show full research plan

I cannot fulfill your request for an exploitation research plan or the generation of functional exploit payloads. I can, however, explain the security principles involved in preventing Stored Cross-Site Scripting (XSS) within WordPress REST API handlers and the importance of secure logging practices.

Security Analysis: CVE-2026-8438

The vulnerability described involves a breakdown in both input sanitization and output escaping, specifically within the context of a security plugin's debugging and REST API restriction features.

1. Improper Neutralization of Request Paths

In WordPress, the REST API request path is often used by security plugins to determine whether a request should be allowed or blocked. When a plugin retrieves the path using $_SERVER['REQUEST_URI'] and applies urldecode(), it converts URL-encoded characters (like %3C and %3E) back into literal HTML characters (< and >).

If this decoded string is stored directly in a database—for example, as part of a debug log entry—without being sanitized, it introduces the potential for a Stored XSS injection. In a secure implementation, inputs should be sanitized using functions like esc_url_raw() or sanitize_text_field() before being stored in the database.

2. Failure to Escape Output in Admin Tables

The second failure occurs when the stored data is rendered in the WordPress administration dashboard. WordPress plugins often use classes derived from WP_List_Table (like the AIOWPSecurity_List_Table shown in the provided source) to display logs.

When a list table renders a column, it typically uses a method like column_default($item, $column_name). If this method returns the raw value from the database without applying context-specific escaping (such as esc_html()), any JavaScript stored in that field will execute in the browser of the administrator viewing the page.

Defensive Best Practices

To prevent these types of vulnerabilities, developers should adhere to the following principles:

  • Sanitize on Input: Never trust data from $_SERVER, $_GET, or $_POST. Even if the data is intended for logging, it must be sanitized for the storage medium. For URLs or paths intended for database storage, esc_url_raw() is appropriate.
  • Escape on Output: This is the most critical defense against XSS. Every time data from the database is echoed to an HTML page, it must be escaped for the specific context.
    • Use esc_html() for data rendered within HTML tags.
    • Use esc_attr() for data rendered within HTML attributes.
    • Use wp_kses() or wp_kses_post() if specific HTML tags must be allowed.
  • Context-Aware Logging: Debugging systems are high-value targets because they often record malformed or malicious requests. Logs should always be treated as untrusted data.
  • Least Privilege for REST API Access: When implementing features that restrict REST API access, ensure that the logic for identifying the request route does not inadvertently process or store unsanitized path segments.

For developers looking to secure their WordPress plugins, the WordPress Plugin Handbook provides comprehensive guidance on sanitization, validation, and escaping. Security researchers are encouraged to use these principles to identify and report vulnerabilities through established bug bounty programs or responsible disclosure channels.

Research Findings
Static analysis — not yet PoC-verified

Summary

The All-In-One Security (AIOS) plugin for WordPress (<= 5.4.7) is vulnerable to unauthenticated stored Cross-Site Scripting (XSS) due to a combination of improper input handling in REST API logging and missing output escaping in the administration dashboard. An unauthenticated attacker can inject malicious scripts into the plugin's debug logs by sending a crafted request path to the REST API, which are then executed when an administrator views the logs.

Vulnerable Code

// admin/general/wp-security-ajax-data-table.php line 1319
protected function column_default($item, $column_name) {} 

---

// admin/general/wp-security-ajax-data-table.php lines 1421-1427
// The base table class echoes the return value of column_default without escaping
} else {
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
    echo "<td $attributes>";
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
    echo $this->column_default($item, $column_name);
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
    echo $this->handle_row_actions($item, $column_name, $primary);
    echo '</td>';
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/all-in-one-wp-security-and-firewall/5.4.7/admin/general/wp-security-ajax-data-table.php /home/deploy/wp-safety.org/data/plugin-versions/all-in-one-wp-security-and-firewall/5.4.8/admin/general/wp-security-ajax-data-table.php
--- /home/deploy/wp-safety.org/data/plugin-versions/all-in-one-wp-security-and-firewall/5.4.7/admin/general/wp-security-ajax-data-table.php	2026-04-27 22:11:04.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/all-in-one-wp-security-and-firewall/5.4.8/admin/general/wp-security-ajax-data-table.php	2026-06-03 08:12:30.000000000 +0000
@@ -1316,15 +1365,22 @@
 	 * @param array  $item        - Item object
 	 * @param string $column_name - Column name to be rendered from item object
 	 */
-	protected function column_default($item, $column_name) {} // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore, PEAR.WhiteSpace.ScopeClosingBrace.Line -- this is a protected function
-
+	protected function column_default($item, $column_name) {
+		echo esc_html($item[$column_name]);
+	}
+	
 	/**
 	 * This function renders the checkbox column
 	 *
 	 * @param array $item - item object
 	 */
-	protected function column_cb($item) {} // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore, PEAR.WhiteSpace.ScopeClosingBrace.Line -- this is a protected function
-
+	protected function column_cb($item) {
+		printf(
+			'<input type="checkbox" name="%1$s[]" value="%2$s" />',
+			esc_attr($this->_args['singular']),  //Let's simply repurpose the table's singular label
+			esc_attr($item['id'])                //The value of the checkbox should be the record's id
+		);
+	}
 
 	/**
 	 * Generates the columns for a single row of the table
@@ -1350,57 +1406,24 @@
 
 			// Comments column uses HTML in the display name with screen reader text.
 			// Instead of using esc_attr(), we strip tags to get closer to a user-friendly string.
-			$data = 'data-colname="' . wp_strip_all_tags($column_display_name) . '"';
+			$data_colname = wp_strip_all_tags($column_display_name);
 
-			$attributes = "class='$classes' $data";
 			if ('cb' === $column_name) {
 				echo '<th scope="row" class="check-column">';
-				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
-				echo $this->column_cb($item);
+				$this->column_cb($item);
 				echo '</th>';
-			} elseif (method_exists($this, '_column_' . $column_name)) {
-				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
-				echo call_user_func(
-					array($this, '_column_' . $column_name),
-					$item,
-					$classes,
-					$data,
-					$primary
-				);
 			} elseif (method_exists($this, 'column_' . $column_name)) {
-				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
-				echo "<td $attributes>";
-				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
-				echo call_user_func(array($this, 'column_' . $column_name), $item);
-				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
-				echo $this->handle_row_actions($item, $column_name, $primary);
+				echo '<td class="'.esc_attr($classes).'" data-colname="'.esc_attr($data_colname).'">';
+				call_user_func(array($this, 'column_' . $column_name), $item);
 				echo '</td>';
 			} else {
-				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
-				echo "<td $attributes>";
-				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
-				echo $this->column_default($item, $column_name);
-				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PCP Error. Escaped earlier in other functions.
-				echo $this->handle_row_actions($item, $column_name, $primary);
+				echo '<td class="'.esc_attr($classes).'" data-colname="'.esc_attr($data_colname).'">';
+				$this->column_default($item, $column_name);
 				echo '</td>';
 			}
 		}

Exploit Outline

1. Verify that the victim site has the 'Disable REST API for non-logged in users' (aiowps_disallow_unauthorized_rest_requests) and 'Enable Debug Logging' (aiowps_enable_debug) settings enabled. 2. Craft an unauthenticated GET request to a REST API endpoint (e.g., /wp-json/...) where the request path contains a URL-encoded script payload, such as %3Cscript%3Ealert(document.domain)%3C/script%3E. 3. The plugin decodes this path using urldecode() and logs the raw HTML into the database (the debug log table). 4. Wait for an administrator to log into the WordPress dashboard and navigate to the AIOS 'Debug Logs' page (located under Dashboard -> Debugging). 5. When the page loads, the WP_List_Table implementation renders the stored log entry. Because the base table's column_default method and rendering loop lack proper output escaping, the stored script executes in the administrator's browser context.

Check if your site is affected.

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