Jul 30, 2026·6 min read·4 visits
An authenticated SSRF vulnerability in Easy!Appointments prior to 1.6.0 allows backend users to probe internal networks and access loopback and RFC1918 addresses via unvalidated CalDAV connection test requests.
Easy!Appointments prior to version 1.6.0 is vulnerable to Server-Side Request Forgery (SSRF) within its CalDAV integration module. The system handles user-supplied URLs in the connection test endpoint without verifying host constraints or network schemes, allowing authenticated backend users to probe internal infrastructure.
Easy!Appointments is an open-source, self-hosted appointment scheduling platform implemented in PHP. The platform provides synchronization capabilities with external calendars using the CalDAV protocol. This functionality is exposed via the controller located at application/controllers/Caldav.php and managed through the Caldav_sync library.
In versions prior to 1.6.0, the application exposes a CalDAV connection verification endpoint designed to test connection parameters to remote CalDAV servers. This feature does not enforce restrictions on the host, protocol, or IP range of the destination URL. Consequently, an authenticated backend user (such as an administrator, provider, or secretary) can manipulate the configuration to initiate outbound connections targeting restricted systems.
Because the backend uses the Guzzle HTTP client to execute these network requests, and handles execution failures by echoing exception details, the vulnerability manifests as a semi-blind Server-Side Request Forgery (SSRF). Attackers can abuse this vector to scan local ports, map internal networks, or query cloud metadata services.
The root cause of this vulnerability lies in the missing network validation within the Caldav::connect_to_server method and its downstream implementation in Caldav_sync::get_http_client. When a user initiates a CalDAV connection test, the backend accepts a user-provided parameter called caldav_url.
The application validates this input using only PHP's native validation filter: filter_var($caldav_url, FILTER_VALIDATE_URL). This check is insufficient for security enforcement. The filter only verifies that the input complies with RFC 2396 syntax for URLs; it does not validate the protocol scheme, target hostname, or destination IP addresses.
When Guzzle executes the subsequent custom HTTP REPORT request, it attempts to resolve the provided address. If the target resides on the loopback interface (127.0.0.1), an internal private subnet (RFC 1918), or a link-local network (169.254.169.254), the server initiates the network connection. If the target server responds with an error or rejects the request, Guzzle throws an exception. The application catches this exception and returns up to 120 bytes of the response body and the HTTP status code inside the JSON payload, providing a highly descriptive information oracle to the caller.
A detailed review of the patches reveals a multi-stage remediation approach. The core security control relies on resolving hostnames before executing requests and validating the resulting IP addresses against reserved ranges.
In the original vulnerable code, the get_http_client function was defined as follows:
private function get_http_client(string $caldav_url, string $caldav_username, string $caldav_password): Client
{
if (!filter_var($caldav_url, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException('Invalid CalDAV URL provided: ' . $caldav_url);
}
// Outbound HTTP client initialization and request execution follow
}The initial security patch in commit 4abb10545d83ac1a57d03f6502376ee67696ea7c replaced this permissive validation with the assert_safe_caldav_url() method. This helper parses the URL scheme and checks whether the resolved IP addresses fall within private or reserved networks:
private function assert_safe_caldav_url(string $caldav_url): void
{
if (!filter_var($caldav_url, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException('Invalid CalDAV URL provided.');
}
$scheme = strtolower((string) parse_url($caldav_url, PHP_URL_SCHEME));
$host = trim((string) parse_url($caldav_url, PHP_URL_HOST), '[]');
if (!in_array($scheme, ['http', 'https'], true) || empty($host)) {
throw new InvalidArgumentException('Invalid CalDAV URL provided.');
}
$resolved_ips = $this->resolve_host_ips($host);
if (empty($resolved_ips)) {
throw new InvalidArgumentException('CalDAV URL host cannot be resolved.');
}
foreach ($resolved_ips as $resolved_ip) {
if (!filter_var($resolved_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
throw new InvalidArgumentException('CalDAV URL host is not allowed.');
}
}
}Additionally, the exception handler was refactored in application/controllers/Caldav.php to suppress raw exception output. Instead of echoing $e->getMessage(), which exposed remote banners, the endpoint now maps errors to static localization strings such as calendar_sync_failed or invalid_credentials_provided.
To exploit this vulnerability, an attacker must first obtain credentials for a backend account with roles such as administrator, provider, or secretary. Once authenticated, the attacker accesses the connection verification feature.
The attacker constructs an HTTP POST request targeting /index.php/caldav/connect_to_server containing a loopback or RFC 1918 address inside the caldav_url parameter:
POST /index.php/caldav/connect_to_server HTTP/1.1
Host: target-scheduler.local
Content-Type: application/x-www-form-urlencoded
Cookie: csrf_cookie_name=...; easyappointments_session=...
caldav_url=http://127.0.0.1:8025/&caldav_username=test&caldav_password=testIf the targeted port is closed, the Guzzle client throws a connection error, which is caught and returned to the attacker's browser as a failed connection message:
{
"success": false,
"message": "cURL error 7: Failed to connect to 127.0.0.1 port 8025: Connection refused..."
}If the targeted port is open, the outbound Guzzle client receives response headers or an error page from the target internal service. Because Guzzle throws a RequestException for unexpected HTTP statuses (like 404 or 405), the controller catches this exception and returns up to 120 bytes of the response body. This lets the attacker fingerprint the software running on the internal asset:
{
"success": false,
"message": "Client error: REPORT http://127.0.0.1:8025 resulted in a 405 Method Not Allowed response: <!DOCTYPE html><html lang=\"en\"><head><title>Mailpit</title>..."
}While the validation routine in 1.6.0 addresses direct requests to internal IP addresses, several advanced exploitation vectors require consideration.
First, Guzzle follows HTTP redirects by default. If the application validates an initial host pointing to a public IP address (e.g., http://malicious-public-domain.com), the check succeeds. However, if that public server responds with an HTTP 302 redirect pointing to an internal loopback URL, Guzzle may follow it internally without re-validating the redirected destination, bypassing pre-flight host validation.
Second, DNS Rebinding poses a risk because of Time-of-Check to Time-of-Use (TOCTOU) discrepancies. During the initial validation, the server resolves the target hostname using PHP's resolver, which returns a benign public IP. When Guzzle subsequently executes the request, it resolves the hostname a second time. If the domain's DNS record is configured with a TTL of 0, the second lookup can resolve to a private IP (e.g., 127.0.0.1), bypassing the validation phase.
Finally, commit 2da2baed18ec32ad7916e507815709c8f010d510 introduced a hardcoded exception for local Docker setups: if ($scheme === 'http' && strtolower($host) === 'baikal') { return; }. While intended for developer utility, any inconsistency between the hostname parsing of PHP's parse_url and Guzzle's PSR-7 parser could potentially be leveraged to bypass host restrictions.
Remediation of CVE-2026-52840 requires upgrading to Easy!Appointments version 1.6.0 or later. The update implements hostname resolution checks, maps Guzzle exception outputs to static error messages, and removes options that allow user-controlled parameters to toggle SSRF safety checks.
For instances where upgrading is not immediately possible, security teams should implement local outbound network constraints. Egress filtering rules at the host or container firewall level (e.g., iptables, Docker network routing) must be applied to block the scheduling application from establishing TCP connections to loopback interfaces and RFC 1918 networks.
Detection can be achieved by monitoring proxy and application logs for unusual hostnames in the CalDAV connection requests. Network intrusion detection systems can also identify outbound HTTP REPORT requests originating from the application server that target local or private IP addresses.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
easyappointments alextselegidis | < 1.6.0 | 1.6.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-918 |
| Attack Vector | Network |
| CVSS v3.1 Score | 2.7 |
| EPSS Score | 0.00186 (Percentile: 8.49%) |
| Exploit Status | poc |
| CISA KEV Status | Not Listed |
The web server receives a physical address or URL from an upstream user and does not validate the host domain or target IP address, allowing unauthorized requests to restricted targets.
An Excessive Data Exposure vulnerability in Easy!Appointments v1.5.2 allows low-privileged administrative users, such as restricted Providers and Secretaries, to harvest unique, stateless appointment hashes belonging to other providers. These hashes act as capability tokens, granting full authorization to reschedule, take over, or delete appointments via stateless endpoints, resulting in a complete Broken Object Level Authorization (BOLA) scenario.
An authorization bypass and logical 'write-before-crash' vulnerability in Easy!Appointments versions prior to 1.6.0 allows authenticated users with the 'Provider' role to inject unauthorized bookings into foreign providers' schedules or reassign existing appointments via parameter manipulation on the 'store' and 'update' endpoints.
An unauthenticated Personally Identifiable Information (PII) disclosure vulnerability exists in Easy!Appointments versions up to and including 1.5.2. Requesting the booking reschedule endpoint with a valid appointment hash causes the system to embed the raw customer database record into the HTML response as inline JavaScript variables, exposing sensitive details. This includes email addresses, phone numbers, physical addresses, custom database metadata, and internal directory configurations such as LDAP DNs. The vulnerability has been resolved in version 1.6.0.
An authorization bypass vulnerability exists in Easy!Appointments before version 1.6.0. The application fails to validate ownership of the provider ID during the Google OAuth synchronization process. This allows authenticated backend users to link their personal Google Calendars to peer providers, leading to unauthorized access to scheduled appointments and associated customer metadata.
This report provides a comprehensive technical teardown of CVE-2026-52838 (GHSA-996f-334j-67g7), a stored Cross-Site Scripting (XSS) vulnerability in Easy!Appointments. The flaw occurs in how the application manages the 'booking disabled' custom message configuration, allowing high-privileged administrators to persist unsanitized payloads that execute on unauthenticated guest landing pages.
CVE-2026-54574 (GHSA-9xq3-3fqg-4vg7) is a critical Symlink Escape and Arbitrary Host File Write vulnerability in proot-distro, an open-source utility for managing rootless PRoot containers on Termux and general Linux environments. The vulnerability is rooted in an asymmetric validation flaw during the archive extraction process of container installations, Docker/OCI layers, and container backup restorations. While the extraction engine successfully validated file names to prevent standard directory traversal (e.g., rejecting components containing '..'), it failed to validate symbolic link targets. An attacker could craft a malicious tar archive or container image that plants an absolute host-path symlink. Subsequent file members within the same archive could then traverse through this symlink, writing arbitrary files directly onto the host filesystem under the privileges of the executing process.