CVE-2026-9109

GPTranslate <= 2.31 - Unauthenticated Stored Cross-Site Scripting via REST API Translation Storage

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

Description

The GPTranslate – Multilingual AI Translation for WordPress: Automatically Translate Websites plugin for WordPress is vulnerable to Stored Cross-Site Scripting via REST API Translation Storage in all versions up to, and including, 2.31 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. The deterministically derived API key (sha256 of the site URL) is printed in the HTML source of every page via the JavaScript variable gptApiKey, meaning any unauthenticated visitor can retrieve the key and submit malicious translation payloads to the /wp-json/gptranslate/v1/request endpoint without any additional precondition.

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<=2.31
PublishedJune 12, 2026
Last updatedJune 13, 2026
Affected plugingptranslate

What Changed in the Fix

Changes introduced in v2.32

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-9109 (GPTranslate Stored XSS) ## 1. Vulnerability Summary The **GPTranslate** plugin (<= 2.31) is vulnerable to **Unauthenticated Stored Cross-Site Scripting (XSS)** via its REST API translation storage endpoint. The plugin uses a deterministically derived AP…

Show full research plan

Exploitation Research Plan - CVE-2026-9109 (GPTranslate Stored XSS)

1. Vulnerability Summary

The GPTranslate plugin (<= 2.31) is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS) via its REST API translation storage endpoint. The plugin uses a deterministically derived API key (SHA256 of the site URL) to authenticate requests to its REST API. Because this key is printed in the HTML source of every page, any unauthenticated visitor can obtain it and submit malicious translation payloads. These payloads are stored in the database and subsequently rendered on the site without proper escaping, leading to XSS.

2. Attack Vector Analysis

  • Endpoint: /wp-json/gptranslate/v1/request
  • Method: POST
  • Authentication: Unauthenticated (requires the x-gptranslate-key header).
  • Vulnerable Parameter: The translated field within the JSON payload.
  • Preconditions:
    1. The plugin must be active.
    2. At least one target translation language must be enabled (e.g., 'es').
    3. The attacker must know the gptApiKey (derived from the site URL).

3. Code Flow

  1. Entry Point (REST API): The plugin registers a REST route at /gptranslate/v1/request (registered via rest_api_init).
  2. Authentication Check: The handler extracts the x-gptranslate-key header and compares it to hash('sha256', get_site_url()).
  3. Storage (Sink): The handler processes the syncTranslation (or similar) task. It accepts an original string and a translated string. It updates the translations column (JSON) in the {prefix}gptranslate table for the specified pagelink.
    • Vulnerability: The translated string is saved to the database without sanitization.
  4. Rendering (Sink): When a user visits the page in the translated language:
    • The plugin hooks into the frontend output (likely via the_content or a similar filter).
    • It retrieves the translations from the database: $response['translations'] = json_decode($row['translations'], true) (seen in ajax-handler.php).
    • It replaces the original text on the page with the stored translated text.
    • Vulnerability: The replacement text is injected into the HTML DOM without output escaping, allowing arbitrary JavaScript execution.

4. Nonce / API Key Acquisition Strategy

The API key required for the REST API is not a standard WordPress nonce, but a deterministic hash.

  1. Extraction: The key is localized to the frontend.
  2. Procedure:
    • Use browser_navigate to the WordPress homepage.
    • Use browser_eval to extract the key from the global JavaScript object:
      // The description mentions gptApiKey variable, admin.js suggests gptranslate_vars
      window.gptApiKey || window.gptranslate_vars?.gptApiKey
      
  3. Alternative Calculation: If JS extraction fails, calculate it manually: hash('sha256', "http://localhost:8080").

5. Exploitation Strategy

Step 1: Identify Site URL and API Key

Obtain the API key using the strategy in Section 4.

Step 2: Submit Malicious Translation

Send a POST request to the REST API to "translate" a common string on the homepage into a malicious payload.

HTTP Request (via http_request tool):

  • URL: http://localhost:8080/wp-json/gptranslate/v1/request
  • Method: POST
  • Headers:
    • Content-Type: application/json
    • x-gptranslate-key: [EXTRACTED_API_KEY]
  • Body:
{
  "task": "syncTranslation",
  "original": "Welcome to WordPress",
  "translated": "Welcome to WordPress<img src=x onerror=alert(document.domain)>",
  "language_translated": "es",
  "pagelink": "http://localhost:8080/",
  "translation_type": "translations"
}

Step 3: Trigger Execution

Navigate to the homepage in the target language (e.g., http://localhost:8080/?lang=es) to trigger the rendering of the malicious translation.

6. Test Data Setup

  1. Install and Activate: Ensure GPTranslate 2.31 is active.
  2. Enable Languages: Use WP-CLI to ensure at least one translation language is configured:
    # Add Spanish ('es') to the gptranslate_options
    wp option patch insert gptranslate_options languages es
    
  3. Identify Target String: Ensure the homepage contains the string "Welcome to WordPress" (standard on default installs).

7. Expected Results

  • The REST API responds with {"result":true} or similar.
  • The database table {prefix}gptranslate now contains the XSS payload in the translations JSON column.
  • Upon visiting the homepage in Spanish, an alert box showing the document domain appears.

8. Verification Steps

  1. Database Check:
    wp db query "SELECT translations FROM wp_gptranslate WHERE languagetranslated = 'es' LIMIT 1;"
    
    Check if the output contains <img src=x onerror=alert(document.domain)>.
  2. Frontend Check:
    Use browser_navigate("http://localhost:8080/?lang=es") and check for the presence of the alert.

9. Alternative Approaches

  • Payload Variance: If <script> tags are filtered by server-side WAFs (though not the plugin), use <img> or <svg> event handlers.
  • Task Names: If syncTranslation fails, try save_translation or update_translation (inferred task names common in these plugins).
  • Target Pages: If the homepage is protected, target a specific post URL by changing the pagelink parameter.
Research Findings
Static analysis — not yet PoC-verified

Summary

GPTranslate (up to version 2.31) is vulnerable to unauthenticated stored Cross-Site Scripting (XSS) because it uses a deterministic and publicly exposed API key for its translation storage REST API. Attackers can leverage this key to submit malicious translation strings that are saved without sanitization and subsequently rendered on the site without escaping.

Vulnerable Code

// ajax-handler.php @ line 55
// Validate API key - simple shared secret hash
$expected_key = hash('sha256', 'gptranslate');

if ($headerApiKey !== $expected_key) {
	http_response_code(403);
	echo json_encode(['result' => false, 'error' => 'Forbidden']);
	exit;
}

---

// assets/js/admin.js (referenced for key exposure)
// The key is often localized to the frontend via gptranslate_vars
headers: {"Content-Type":"application/json; charset=utf-8", Accept:"application/json", "x-gptranslate-key": gptranslate_vars.gptApiKey}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/gptranslate/2.31/ajax-handler.php /home/deploy/wp-safety.org/data/plugin-versions/gptranslate/2.32/ajax-handler.php
--- /home/deploy/wp-safety.org/data/plugin-versions/gptranslate/2.31/ajax-handler.php	2026-05-16 00:16:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/gptranslate/2.32/ajax-handler.php	2026-05-21 23:00:22.000000000 +0000
@@ -52,10 +52,16 @@
 	}
 }
 
-// Validate API key - simple shared secret hash
-$expected_key = hash('sha256', 'gptranslate');
+// Validate API key - cryptographically random secret stored in wp_options.
+// SHORTINIT does not load the options API, so query the option table directly.
+$expected_key = $wpdb->get_var(
+	$wpdb->prepare(
+		"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
+		'gptranslate_api_secret'
+	)
+);
 
-if ($headerApiKey !== $expected_key) {
+if ($expected_key === null || $expected_key === '' || !is_string($headerApiKey) || !hash_equals((string) $expected_key, $headerApiKey)) {
 	http_response_code(403);
 	echo json_encode(['result' => false, 'error' => 'Forbidden']);
 	exit;

Exploit Outline

1. Extract the deterministic API key from the target site's frontend (usually found in global JavaScript variables like 'gptApiKey' or 'gptranslate_vars.gptApiKey'). 2. Send an unauthenticated POST request to the `/wp-json/gptranslate/v1/request` REST endpoint (or the custom ajax-handler.php endpoint if applicable). 3. Set the 'x-gptranslate-key' header to the extracted API key. 4. Craft a JSON payload with the 'task' set to 'syncTranslation' and the 'translated' parameter containing a malicious script (e.g., <script>alert(1)</script>). 5. Target a specific page by providing its URL in the 'pagelink' parameter and specify a target language (e.g., 'es'). 6. Navigate to the targeted page in that language to trigger the execution of the stored XSS payload.

Check if your site is affected.

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