Security

Overview

WPZylos Scheduler executes user-defined code on a recurring basis. This has inherent security considerations that must be addressed in your implementation.

Task Callback Security

Never Trust User Input in Tasks

Scheduled tasks may interact with the database, external APIs, or the filesystem. Always sanitize and validate:

// Good: Good — sanitized query
$schedule->call(function () {
    global $wpdb;
    $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM {$wpdb->prefix}tokens WHERE user_id = %d AND expires_at < %s",
            $user_id,
            current_time('mysql')
        )
    );
})->daily()->name('clean-tokens');

// Bad: Bad — unsanitized input
$schedule->call(function () {
    global $wpdb;
    $wpdb->query("DELETE FROM {$wpdb->prefix}tokens WHERE status = '{$_GET['status']}'");
})->daily()->name('unsafe-cleanup');

Escape Output

If tasks generate output (emails, logs), escape content:

$schedule->call(function () {
    $data = get_option('report_data');
    wp_mail(
        get_option('admin_email'),
        esc_html('Daily Report'),
        wp_kses_post($data)
    );
})->dailyAt('08:00')->name('daily-report');

Lock Security

Transient-Based Locks

Locks are stored as WordPress transients. Be aware:

  • Database-backed: In default WordPress, transients may be stored in the wp_options table
  • Object cache: If an object cache (Redis, Memcached) is configured, transients use it instead
  • Expiration: Always set appropriate lock expiration times to prevent deadlocks
// Set reasonable lock durations
$schedule->call($longTask)
    ->withoutOverlapping(120)  // 2 hours max
    ->name('import-task');

Lock Key Collision

Task names are hashed with MD5 for lock keys. While collision probability is extremely low, always use unique, descriptive task names:

// Good: Good — unique names
$schedule->call($fn)->name('myplugin-clean-tokens');
$schedule->call($fn)->name('myplugin-sync-users');

// Bad: Bad — generic names that might collide across plugins
$schedule->call($fn)->name('cleanup');
$schedule->call($fn)->name('sync');

WP-CLI Command Security

When scheduling WP-CLI commands, never include user-controllable input:

// Good: Good — static command
$schedule->command('cache flush')->hourly()->name('cache-flush');

// Bad: Bad — dynamic command from option (could be manipulated)
$command = get_option('custom_command');
$schedule->command($command)->hourly()->name('custom-cmd');

Job Class Security

When scheduling job classes, only use trusted, autoloaded classes:

// Good: Good — known class
$schedule->job(ProcessOrders::class)->everyFiveMinutes()->name('process-orders');

// Bad: Bad — class name from user input
$schedule->job($_POST['job_class'])->everyFiveMinutes()->name('dynamic-job');

Capability Checks

If tasks perform privileged operations, verify capabilities even in cron context:

$schedule->call(function () {
    // Although running in cron context, verify this is a legitimate execution
    if (!wp_doing_cron() && !current_user_can('manage_options')) {
        return;
    }

    // Privileged operation
    delete_option('sensitive_data');
})->daily()->name('admin-cleanup');

Error Information Disclosure

The wpzylos_scheduler_task_failed action receives the full exception. Don't expose internal details to end users:

add_action('wpzylos_scheduler_task_failed', function ($task, $e) {
    // Good: Good — log internally
    error_log("[Scheduler] {$task->getName()} failed: {$e->getMessage()}");

    // Bad: Bad — don't expose to frontend
    // echo $e->getTraceAsString();
}, 10, 2);

Reporting Vulnerabilities

If you discover a security vulnerability in WPZylos Scheduler, please report it responsibly. See security.md for instructions.