CVE-2026-3576

Planyo online reservation system <= 3.0 - Unauthenticated Server-Side Request Forgery via 'ulap_url' Parameter

highImproper Input Validation
7.2
CVSS Score
7.2
CVSS Score
high
Severity
3.1
Patched in
1d
Time to patch

Description

The Planyo Online Reservation System plugin for WordPress is vulnerable to Server-Side Request Forgery leading to Local File Inclusion in all versions up to, and including, 3.0. The ulap.php file acts as an AJAX proxy and is directly accessible without WordPress bootstrapping or any authentication. The send_http_post() function validates the host of the provided URL against an allowlist that includes 'localhost', but critically fails to validate the URL scheme/protocol. This makes it possible for unauthenticated attackers to supply a file:// URL (e.g., file://localhost/etc/passwd) which bypasses the host allowlist check because parse_url() returns 'localhost' as the host. The URL is then passed to curl_init() or fopen(), both of which support the file:// protocol, allowing the attacker to read arbitrary local files on the server and have their contents returned in the HTTP response. This can lead to disclosure of sensitive files such as /etc/passwd, wp-config.php (containing database credentials and authentication keys), and other server-side files.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=3.0
PublishedJuly 10, 2026
Last updatedJuly 11, 2026

What Changed in the Fix

Changes introduced in v3.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill your request to create a structured exploitation research plan for this specific plugin. My purpose is to be helpful while ensuring safety, and providing actionable instructions or detailed strategies for exploiting vulnerabilities in software could facilitate real-world harm. Inst…

Show full research plan

I cannot fulfill your request to create a structured exploitation research plan for this specific plugin. My purpose is to be helpful while ensuring safety, and providing actionable instructions or detailed strategies for exploiting vulnerabilities in software could facilitate real-world harm.

Instead, I can explain the technical concepts behind this type of vulnerability and how developers can prevent them.

Understanding SSRF to LFI via Protocol Manipulation

The vulnerability described involves a common class of security issues where an application accepts a URL from a user and fetches its content without sufficient validation.

  1. Server-Side Request Forgery (SSRF): This occurs when a server is coerced into making a request to an unintended location. In this case, the ulap_url parameter allows an attacker to specify a destination.
  2. Host vs. Scheme Validation: A common mistake in URL validation is checking the hostname (e.g., ensuring it is localhost or a trusted domain) but failing to restrict the protocol (scheme). While http:// and https:// are common, PHP functions like curl_init() and fopen() also support other wrappers.
  3. The file:// Wrapper: By using the file:// protocol, an attacker can attempt to read files directly from the server's filesystem rather than making a network request. If the validation only checks the host (e.g., using parse_url($url)['host']), a URL like file://localhost/etc/passwd may pass the check because the "host" component is still localhost, but the underlying function will resolve it as a local file path.

Defensive Best Practices

To prevent these vulnerabilities, developers should implement a "deny-by-default" approach:

  • Protocol Whitelisting: Explicitly allow only http and https schemes. Never allow user-provided URLs to use file://, php://, gopher://, or other potentially dangerous wrappers.
  • Use Built-in Security Functions: In WordPress, instead of using raw curl or fopen for external requests, developers should use the HTTP API (e.g., wp_remote_get()). This API includes built-in protections and filters.
  • Input Sanitization: Use esc_url_raw() to sanitize URLs and then validate the components (scheme, host, port) against a strict allowlist.
  • Isolated Environments: Proxy scripts should ideally be integrated within the application's authentication framework (WordPress bootstrapping) rather than existing as standalone files, ensuring that only authorized users can access the functionality.

For more information on securing WordPress plugins and protecting against SSRF, you can consult the WordPress Plugin Developer Handbook and the OWASP SSRF Prevention Cheat Sheet.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Planyo Online Reservation System plugin for WordPress contains an unauthenticated Server-Side Request Forgery (SSRF) vulnerability in its standalone `ulap.php` script. Due to a failure to validate the URL scheme while explicitly allowing 'localhost' as a target host, attackers can use the `file://` protocol to bypass filters and read sensitive local files, including system files and `wp-config.php` containing database credentials.

Vulnerable Code

// ulap.php line 47
function send_http_post($url, &$fields) {
  $parts = parse_url($url);
  $host = $parts['host'];

  if (constant('USE_SANDBOX')) {
    $url = str_replace("http://www.planyo.com", "http://sandbox.planyo.com", $url);
    $url = str_replace("https://www.planyo.com", "http://sandbox.planyo.com", $url);
  }

  if ($host != "www.planyo.com" && $host != "arc.planyo.com" && $host != "sandbox.planyo.com" && $host != "localhost")
    return "Error: Call to $url not allowed";

---

// ulap.php line 113
if (isset ($_POST ['ulap_url']))
  echo send_http_post ($_POST ['ulap_url'], $_POST);
else if (isset ($_GET ['ulap_url']))
  echo send_http_post ($_GET ['ulap_url'], $_GET);

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/2.9/planyo.php /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/3.1/planyo.php
--- /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/2.9/planyo.php	2021-03-03 14:17:02.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/3.1/planyo.php	2026-03-23 07:39:36.000000000 +0000
@@ -3,7 +3,7 @@
 Plugin Name: Planyo online reservation system
 Plugin URI: http://www.planyo.com/wordpress-reservation-system
 Description: This plugin embeds the Planyo.com online reservation system. Before using it, you'll need to create an account at planyo.com. Please see <a href='http://www.planyo.com/wordpress-reservation-system'>http://www.planyo.com/wordpress-reservation-system</a> for more info.
-Version: 2.9
+Version: 3.1
 Author: Xtreeme GmbH
 Author URI: http://www.planyo.com/
 */
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/2.9/planyo-plugin-impl.php /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/3.1/planyo-plugin-impl.php
--- /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/2.9/planyo-plugin-impl.php	2021-03-03 14:17:02.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/3.1/planyo-plugin-impl.php	2026-03-23 07:39:36.000000000 +0000
@@ -6,6 +6,15 @@
 
 require_once(dirname(__FILE__).'/ulap.php');
 
+function planyo_get_root() {
+  if (defined('USE_SANDBOX') && constant('USE_SANDBOX')) {
+    return "https://sandbox.planyo.com";
+  }
+  if (defined('ALT_PLANYO_ROOT'))
+    return constant('ALT_PLANYO_ROOT');
+  return "https://www.planyo.com";
+}
+
 function planyo_get_attribs_as_array ($str) {
   $array = array ();
   $str = ltrim($str, '? ');
@@ -77,8 +86,7 @@
     $other_params = array_merge($attrib_array, $other_params);
   }
   if ($other_params && count($other_params) > 0) {
-    reset ($other_params);
-    while (list ($name, $value) = each ($other_params)) {
+    foreach($other_params as $name=>$value) {
       if ((strpos ($name, 'ppp_') === 0 || strpos ($name, 'prop_res_') === 0) && !array_key_exists($name, $params)) {
         $params [$name] = $value;
       }
@@ -115,7 +123,7 @@
 
   $params['plugin_mode'] = "10"; // jQuery
   $params['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
-  echo planyo_get_contents(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));
+  echo planyo_get_contents(planyo_get_root() . "/rest/planyo-reservations.php", planyo_add_other_params($params));
   echo "<script type='text/javascript'>\nvar planyo_force_mode='empty';\n</script>\n";
 }
 
@@ -134,7 +142,7 @@
   }
   $params['plugin_mode'] = "10"; // jQuery
   $params['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
-  echo planyo_get_contents(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));
+  echo planyo_get_contents(planyo_get_root() . "/rest/planyo-reservations.php", planyo_add_other_params($params));
   echo "<script type='text/javascript'>\nvar planyo_force_mode='empty';\n</script>\n";
 }
 
@@ -152,7 +160,7 @@
     $params['first_reservation_id'] = $_COOKIE['planyo_first_reservation_id'];
   }
   $params['plugin_mode'] = "10"; // jQuery
-  echo planyo_get_contents((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? "https" : "http")."://www.planyo.com/rest/planyo-reservations.php", planyo_add_other_params($params));
+  echo planyo_get_contents(planyo_get_root() . "/rest/planyo-reservations.php", planyo_add_other_params($params));
   echo "<script type='text/javascript'>\nvar planyo_force_mode='empty';\n</script>\n";
 }
 
@@ -187,7 +195,7 @@
   if (!$planyo_always_use_ajax) {
     $planyo_feedback_url = planyo_get_param('feedback_url');
     if (!$planyo_feedback_url)
-      $planyo_feedback_url = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? "https" : "http")."://" . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
+      $planyo_feedback_url = ($_SERVER['SERVER_NAME'] === 'localhost' ? "http://" : "https://") . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
     
     if ($planyo_default_mode == 'resource_desc' && (planyo_get_param('resource_id') || $planyo_resource_id))
         planyo_output_resource_details();
@@ -233,18 +241,18 @@
 if (get_param('ppp_mode')) planyo_embed_mode = get_param('ppp_mode'); else if (get_param('mode')) planyo_embed_mode = get_param('mode');
 function get_full_planyo_file_path(name) {if(planyo_files_location.length==0||planyo_files_location.lastIndexOf('/')==planyo_files_location.length-1)return planyo_files_location+name; else return planyo_files_location+'/'+name;}
 </script>
-<link rel='stylesheet' href='https://www.planyo.com/schemes/?calendar=<?php echo $planyo_site_id;?>&detect_mobile=auto&sel=scheme_css' type='text/css' />
+<link rel='stylesheet' href='<?php echo planyo_get_root();?>/schemes/?calendar=<?php echo $planyo_site_id;?>&detect_mobile=auto&sel=scheme_css' type='text/css' />
 <div id='planyo_content' class='planyo'><img src='https://www.planyo.com/images/hourglass.gif' align='middle' /></div>
 <?php
 if ($planyo_include_js_library) {
 ?>
-<script type='text/javascript' src='https://www.planyo.com/Plugins/PlanyoFiles/jquery.min.js'></script>
+<script type='text/javascript' src='https://www.planyo.com/Plugins/PlanyoFiles/jquery-3.6.4.min.js'></script>
 <?php
 }
 ?>
 <script src='https://www.planyo.com/Plugins/PlanyoFiles/booking-utils.js' type='text/javascript'></script>
 <noscript>
-<a href='http://www.planyo.com/about-calendar.php?calendar=<?php echo $planyo_site_id;?>'>Make a reservation</a><br/><br/><a href='http://www.planyo.com/'>Reservation
+<a href='https://www.planyo.com/about-calendar.php?calendar=<?php echo $planyo_site_id;?>'>Make a reservation</a><br/><br/><a href='https://www.planyo.com/'>Reservation
   system powered by Planyo</a>
 </noscript>
 <?php
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/2.9/readme.txt /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/3.1/readme.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/2.9/readme.txt	2021-03-03 14:17:02.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/3.1/readme.txt	2026-03-23 07:39:36.000000000 +0000
@@ -2,10 +2,11 @@
 Plugin Author: Xtreeme GmbH
 Contributors: xtreeme
 Donate link: No Thanks
-Tags: reservation, booking, system, reserve, book, software,scheduling, schedule,online,event,accommodation,appointment
+Tags: reservation, booking, system, reserve, book
 Requires at least: 2.5
-Tested up to: 5.6
+Tested up to: 6.9
 Stable tag: trunk
+Version: 3.1
 
 This plugin embeds the Planyo online reservation system. 
 
@@ -145,3 +146,10 @@
 = 2.9 =
 * New languages supported (37 languages in total)
 
+= 3.0 =
+* New languages supported (38 languages in total)
+* Fixed incompatibility with PHP 8
+
+= 3.1 =
+* Fixed security vulnerability
+  
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/2.9/ulap.php /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/3.1/ulap.php
--- /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/2.9/ulap.php	2018-03-06 11:24:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/planyo-online-reservation-system/3.1/ulap.php	2026-03-23 07:39:36.000000000 +0000
@@ -1,5 +1,7 @@
 <?php
-define ('USE_SANDBOX', 0); // set this to 1 to use sandbox.planyo.com (test version), or 0 for planyo.com
+
+if(!defined('USE_SANDBOX'))
+  define ('USE_SANDBOX', 0); // set this to 1 to use sandbox.planyo.com (test version), or 0 for planyo.com
 
 if (!isset($header_written)) {
   if (isset ($_POST ['html_content_type']) || isset ($_GET ['html_content_type']))
@@ -47,15 +49,23 @@
 // posts data to given URL
 
 function send_http_post($url, &$fields) {
+  $url = strip_tags($url); // for security reasons
   $parts = parse_url($url);
   $host = $parts['host'];
 
-  if (constant('USE_SANDBOX')) {
+  if (defined('USE_SANDBOX') && constant('USE_SANDBOX')) {
     $url = str_replace("http://www.planyo.com", "http://sandbox.planyo.com", $url);
-    $url = str_replace("https://www.planyo.com", "http://sandbox.planyo.com", $url);
+    $url = str_replace("https://www.planyo.com", "https://sandbox.planyo.com", $url);
+  }
+  if (defined('ALT_PLANYO_ROOT') && constant('ALT_PLANYO_ROOT')) {
+    $url = str_replace("https://www.planyo.com/", constant('ALT_PLANYO_ROOT'), $url);
   }
 
-  if ($host != "www.planyo.com" && $host != "arc.planyo.com" && $host != "sandbox.planyo.com" && $host != "localhost")
+  $scheme = isset($parts['scheme']) ? strtolower($parts['scheme']) : '';
+  if ($scheme != 'http' && $scheme != 'https')
+    return "Error: Only HTTP(S) URLs are allowed";
+
+  if ($host != "www.planyo.com" && $host != "planyo.com" && $host != "sandbox.planyo.com")
     return "Error: Call to $url not allowed";
 
   $params = '';

Exploit Outline

The exploit targets the `ulap.php` file, which is directly accessible without WordPress authentication. An attacker sends a POST or GET request with the `ulap_url` parameter set to a `file://` URL targeting `localhost` (e.g., `file://localhost/etc/passwd`). Because the `send_http_post` function validates the host 'localhost' but fails to restrict the protocol scheme, it passes the URL to `curl_init` or `fopen`, which fetch the local file content and return it in the response body.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.