The Exploit
An unauthenticated attacker can export the complete activity time database as CSV by issuing a single GET request to the plugin's export handler.
GET /wp-admin/admin-ajax.php?action=activity_time_csv_url&url_export=1 HTTP/1.1
Host: target.wordpress.local
Connection: close
The server responds with a CSV file containing all tracked user activity timestamps, session data, and activity metadata. No authentication token, session cookie validation, or capability check occurs before the export executes — the function simply checks whether the url_export GET parameter exists, then streams the database contents to the requester's browser.
What the Patch Did
Before
function activity_time_csv_url()
{
if (!isset($_GET['url_export'])) return;
ob_clean();
global $wpdb;
// ... proceeds directly to CSV export
}
After
function activity_time_csv_url()
{
if (!isset($_GET['url_export'])) return;
// Require login
if (!is_user_logged_in()) {
wp_die('Unauthorized', 403);
}
// Require admin capability
if ( ! current_user_can( 'administrator' ) ) {
exit();
}
ob_clean();
global $wpdb;
// ... proceeds to CSV export only if authenticated as admin
}
The patch introduces two sequential authentication gates. The first call to is_user_logged_in() rejects any request from an unauthenticated visitor by terminating execution with a 403 Unauthorized response. The second gate, current_user_can('administrator'), ensures that even logged-in users lacking administrative capability cannot access the export. These are WordPress's standard authentication and authorization APIs — is_user_logged_in() checks whether the request carries a valid user session, while current_user_can() evaluates role-based access control against the global $current_user object populated during the request bootstrap.
Root Cause
CWE-862: Missing Authorization
The activity_time_csv_url() function is registered to fire on the wp_footer hook and processes the url_export GET parameter without verifying that the requester holds a user session or administrative role. The parameter name url_export is attacker-controlled — it is read directly from $_GET and its presence alone determines whether the export logic executes. No intermediate validation, nonce verification, or capability delegation occurs between parameter arrival and the database query that populates the CSV. The function crosses the trust boundary from unauthenticated request to privileged database access without collecting proof of identity or authorization. Because the export operates within the WordPress admin context (via admin-ajax.php), it inherits no implicit session validation; the developer must enforce it explicitly.
Why It Works
The load-bearing line is if (!is_user_logged_in()) wp_die('Unauthorized', 403); — remove it and an unauthenticated attacker continues to the current_user_can() check, which will fail because $current_user is populated as the anonymous user object with no capabilities. However, the engineer added current_user_can('administrator') as defence-in-depth because it creates a second boundary that stops authenticated non-admin users (e.g., subscribers or editors). If only is_user_logged_in() existed and a subscriber's session were hijacked via XSS, or if a site permitted self-registration, the export would still be accessible to low-privilege accounts. The combination ensures that (1) you must hold a valid WordPress session, and (2) that session must belong to an administrator — a dual-gate design that mirrors WordPress's own admin pages, which also check both conditions in sequence.
Hardening Checklist
-
Check user capability for all admin-facing exports. Before processing any
$_GETor$_POSTparameter that triggers data access or state change, callis_user_logged_in()andcurrent_user_can()with the appropriate capability (typically'manage_options'for site-wide exports). WordPress evaluates these checks against the current request's user context; they must occur before any database or file operation. -
Protect AJAX handlers with nonce verification. Register AJAX actions via
add_action('wp_ajax_admin_*', ...)to restrict to authenticated admins only, or usewp_verify_nonce($_GET['nonce'], 'action_name')to cryptographically bind the request to a specific page load and user, preventing cross-site forgery. -
Log and monitor data export requests. Even after adding capability checks, log all invocations of sensitive functions like CSV export to a persistent audit trail, including timestamp, user ID, and count of rows exported. This enables detection of compromised admin sessions that abuse the export feature.
-
Validate parameter presence and type. Replace bare
isset()checks with explicit type validation — e.g.,filter_var($_GET['url_export'], FILTER_VALIDATE_BOOLEAN)— to prevent logic errors if the parameter is present but malformed. -
Use
wp_safe_remote_post()for external plugin updates. If this plugin receives updates from a custom repository, verify the response signature and usewp_safe_remote_post()rather than direct cURL calls to inherit WordPress's security filters and SSL peer verification.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-32362