CVE-2026-1921

Loco Translate <= 2.8.2 - Authenticated (Translator+) Path Traversal to Limited File Read via 'ref' Parameter

mediumImproper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
4.9
CVSS Score
4.9
CVSS Score
medium
Severity
2.8.3
Patched in
1d
Time to patch

Description

The Loco Translate plugin for WordPress is vulnerable to Path Traversal in all versions up to, and including, 2.8.2 via the `fsReference` AJAX route. This is due to the `findSourceFile()` method normalizing user-supplied `ref` paths containing `../` directory traversal sequences without validating that the resolved path remains within the intended bundle or content directory. This makes it possible for authenticated attackers, with Translator-level access and above (custom `loco_admin` capability required, granted to the `translator` role and administrators by default), to read arbitrary `.php`, `.js`, `.json`, and `.twig` files from the server filesystem outside the intended translation directory. Files named wp-config.php are excluded.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
High
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=2.8.2
PublishedMay 4, 2026
Last updatedMay 5, 2026
Affected pluginloco-translate

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps to exploit **CVE-2026-1921**, a Path Traversal vulnerability in the Loco Translate plugin for WordPress. ## 1. Vulnerability Summary The Loco Translate plugin (versions <= 2.8.2) contains a path traversal vulnerability within its `fsReference` AJAX route. The v…

Show full research plan

This research plan outlines the steps to exploit CVE-2026-1921, a Path Traversal vulnerability in the Loco Translate plugin for WordPress.

1. Vulnerability Summary

The Loco Translate plugin (versions <= 2.8.2) contains a path traversal vulnerability within its fsReference AJAX route. The vulnerability exists in the findSourceFile() method, which is responsible for locating source files based on a reference string provided in the ref parameter. While the method attempts to normalize the path, it fails to validate that the resulting absolute path remains within the boundaries of the intended translation bundle or the WordPress content directory.

This allows an authenticated user with the loco_admin capability (default for Administrators and the "Translator" role) to read the contents of sensitive files ending in .php, .js, .json, or .twig.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: loco_fs_reference (mapped to the fsReference logic).
  • Vulnerable Parameter: ref
  • Required Capability: loco_admin (Authenticated).
  • Constraints:
    • The file must exist and have an extension of .php, .js, .json, or .twig.
    • Files named wp-config.php are explicitly excluded by the plugin's logic.

3. Code Flow (Inferred from Patch and Logic)

  1. Entry Point: The plugin registers an AJAX handler for loco_fs_reference (likely in src/ajax/Action.php or src/hooks/AdminHooks.php).
  2. Controller: The request is routed to a controller method (e.g., Loco_ajax_Action::fsReference).
  3. Parameter Handling: The ref parameter is passed to a file-finding utility, specifically findSourceFile().
  4. Vulnerable Logic (Sink):
    • findSourceFile() receives the ref string (e.g., ../../../../wp-includes/version.php).
    • The method uses path normalization but does not verify if the file is within a "safe" directory.
    • If the file exists and passes the extension check, the plugin reads and returns its contents (often for display in the translation editor's "Source" view).
  5. Response: The file content is returned in the AJAX JSON response.

4. Nonce Acquisition Strategy

Loco Translate protects its AJAX actions with a nonce. This nonce is typically localized into the locoConf JavaScript object on any Loco Translate admin page.

  1. Identify Target Page: Any Loco Translate admin page, such as /wp-admin/admin.php?page=loco-plugin.
  2. User Level: Log in as an Administrator (who inherently has loco_admin).
  3. Extraction via Browser:
    • Navigate to the Loco Translate dashboard.
    • Execute JavaScript to retrieve the nonce from the locoConf global variable.
    • JS Key: window.locoConf.nonce (or window.loco.conf.nonce based on versioning).

5. Exploitation Strategy

The goal is to read wp-includes/version.php to prove arbitrary file read (excluding wp-config.php).

Step 1: Authentication

Authenticate as a user with loco_admin (Administrator).

Step 2: Extract Nonce

Use the browser to navigate to the Loco Translate "Home" page and extract the nonce.

  • URL: http://localhost:8080/wp-admin/admin.php?page=loco
  • JS: browser_eval("window.locoConf.nonce")

Step 3: Execute Path Traversal

Send a POST request to admin-ajax.php using the http_request tool.

Request Details:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=loco_fs_reference&ref=../../../../wp-includes/version.php&_wpnonce=[EXTRACTED_NONCE]
    

Step 4: Analyze Response

The response should be a JSON object. If successful, the content or data field will contain the PHP source code of wp-includes/version.php.

6. Test Data Setup

  1. Install Plugin: Ensure Loco Translate <= 2.8.2 is installed and active.
  2. User Creation: Ensure at least one Administrator user exists.
  3. Environmental Check: Confirm that wp-includes/version.php exists (standard in all WordPress installs).

7. Expected Results

  • Success: The HTTP response status is 200, and the JSON body contains the string $wp_version = '...'; which is characteristic of the target file.
  • Failure (Patched): The response returns an error message indicating the file is outside the permitted directory or a 403 Forbidden.
  • Failure (Wrong Extension): Attempting to read /etc/passwd should fail because it does not end in .php, .js, .json, or .twig.

8. Verification Steps

  1. Compare Content: Compare the output returned in the http_request response with the actual content of the file on disk using WP-CLI.
    • Command: wp eval "echo file_get_contents(ABSPATH . 'wp-includes/version.php');"
  2. Capability Check: Verify the loco_admin requirement by attempting the same request with a "Subscriber" user's cookie and confirming a failure.

9. Alternative Approaches

If wp-includes/version.php is blocked by server-side security (unlikely for file read), try targeting:

  • A theme file: ../../../../wp-content/themes/twentytwentyfour/functions.php
  • A plugin file: ../../../../wp-content/plugins/hello.php
  • A JSON file: ../../../../wp-content/plugins/loco-translate/composer.json

If the nonce is not in window.locoConf, search the page source for any script tag containing "nonce" or "loco". Use:
browser_eval("document.documentElement.innerHTML.match(/\"nonce\":\"([a-f0-9]+)\"/)[1]")

Research Findings
Static analysis — not yet PoC-verified

Summary

The Loco Translate plugin for WordPress is vulnerable to path traversal through the 'ref' parameter in its fsReference AJAX route. Authenticated users with the 'loco_admin' capability can exploit this to read sensitive file contents on the server, provided the files have .php, .js, .json, or .twig extensions and are not named wp-config.php.

Vulnerable Code

// Inferred from Loco Translate's AJAX handling and findSourceFile logic
// src/ajax/Action.php (approximate)

public function fsReference() {
    $ref = isset($_POST['ref']) ? (string) $_POST['ref'] : '';
    
    // findSourceFile resolves the path but lacks boundary validation
    $file = $this->findSourceFile($ref);

    if ($file && $file->exists()) {
        // Extension check exists, but traversal allows escaping directory
        if ($file->isSource()) {
             return array( 'content' => $file->getContents() );
        }
    }
}

// src/fs/File.php (approximate)
public function findSourceFile( $ref ) {
    // Normalization occurs without checking if the result is within safe roots
    $path = $this->root . '/' . $ref;
    return new Loco_fs_File( $path );
}

Security Fix

--- a/src/ajax/Action.php
+++ b/src/ajax/Action.php
@@ -102,6 +102,12 @@
     public function fsReference() {
         $ref = isset($_POST['ref']) ? (string) $_POST['ref'] : '';
         $file = $this->findSourceFile($ref);
+
+        // Validate that the resolved file path is within the permitted WordPress content or plugin directory
+        $abs_path = $file->getPath();
+        if ( ! $this->isWithinSafeDirectory($abs_path) ) {
+            throw new Loco_error_Exception('Access denied to file outside of project scope');
+        }
 
         if ($file && $file->exists()) {
             if ($file->isSource()) {

Exploit Outline

1. Authenticate as a user with the 'loco_admin' capability (typically Administrator or the 'Translator' role). 2. Navigate to the Loco Translate admin dashboard and extract the AJAX nonce from the JavaScript source (found in `window.locoConf.nonce`). 3. Craft an AJAX POST request to `/wp-admin/admin-ajax.php` with the following parameters: - action: `loco_fs_reference` - _wpnonce: [Extracted Nonce] - ref: A traversal string targeting a sensitive file with an allowed extension (e.g., `../../../../wp-includes/version.php` or `../../../../wp-content/themes/twentytwentyfour/functions.php`). 4. Analyze the JSON response, which will contain the raw source code of the requested file in the 'content' field.

Check if your site is affected.

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