Jul 30, 2026·6 min read·22 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.
A critical security vulnerability has been identified and patched in the WebUI component of pyLoad, a popular Python-based open-source download manager. This vulnerability allows attackers to completely bypass API rate limits and spoof client IP addresses in audit logs due to unsafe parsing of the X-Forwarded-For HTTP header.
An authorization bypass and credential oracle vulnerability in pyload-ng allows authenticated users with minimal or no privileges to brute-force the administrator password. This is achieved through sensitive API methods exposed globally combined with a non-constant-time password hash comparison algorithm.
An authentication bypass vulnerability in pyLoad allows unauthenticated remote attackers to gain administrative API access. The vulnerability is caused by a logical flaw in the API key cache validation lookup, where authentication states are cached using only the public key identifier, skipping cryptographic token verification on cache hits.
An authorization bypass vulnerability in Strawberry GraphQL between versions 0.217.0 and 0.326.1 occurs when a synchronous permission handler returns an awaitable object (such as an unawaited coroutine). Due to Python's truthiness rules, the unawaited coroutine is evaluated as True, leading to an immediate bypass of security policies.
An uncontrolled resource consumption vulnerability in Strawberry GraphQL allows unauthenticated remote attackers to trigger a Denial of Service on persistent WebSocket connections using the legacy graphql-ws protocol. When the server enforces max_subscriptions_per_connection, naturally terminating subscriptions are not cleared from memory registries, leading to exhaustion of connection slots.
A high-severity type-confusion vulnerability exists in NearForm fast-jwt prior to version 6.3.0. The vulnerability allows attackers to bypass crucial claim validation steps (such as expiration, issuer, audience, and subject validations) by presenting a validly signed JSON Web Token structured as a JSON array instead of a JSON object. This occurs because the library's decoder fails to reject JSON arrays during type evaluation.