Security

Security considerations for WPZylos Routing.

Nonce Verification

AJAX Routes

AJAX routes verify nonces automatically by default:

// Nonce is verified automatically
$ajax->private('save_settings', [SettingsController::class, 'save']);

Only disable for truly public, read-only endpoints:

// OK: public search with no side effects
$ajax->public('search', [SearchController::class, 'handle'])
    ->withoutNonce();

// DANGER: never disable for state-changing operations
$ajax->private('delete_post', [PostController::class, 'delete'])
    ->withoutNonce(); // DON'T DO THIS!

Generating Nonces

// Generate nonce for frontend
$ajax->nonce('save_settings');
$ajax->nonceField('save_settings');

Permission Checks

REST API Permissions

Always use permission callbacks for protected endpoints:

// Capability-based
$rest->delete('/posts/{id}', [PostController::class, 'destroy'])
    ->permission('delete_others_posts');

// Custom callback for per-resource permissions
$rest->delete('/posts/{id}', [PostController::class, 'destroy'])
    ->setPermissionCallback(function ($request) {
        $post_id = $request->get_param('id');
        return current_user_can('delete_post', $post_id);
    });

AJAX Permissions

Check permissions in your controller:

class SettingsController
{
    public function save(): array
    {
        if (!current_user_can('manage_options')) {
            throw new \Exception('Permission denied');
        }

        // Proceed with save
    }
}

Input Sanitization

Always Sanitize User Input

class PostController
{
    public function store(\WP_REST_Request $request): array
    {
        // Sanitize all input
        $title = sanitize_text_field($request->get_param('title'));
        $content = wp_kses_post($request->get_param('content'));
        $email = sanitize_email($request->get_param('email'));
        $url = esc_url_raw($request->get_param('url'));
        $number = absint($request->get_param('count'));

        // Never trust raw input
        // $raw = $request->get_param('unsafe'); // DANGEROUS
    }
}

REST API Argument Validation

Use WordPress's built-in validation:

$rest->post('/users', [UserController::class, 'store'])
    ->args([
        'email' => [
            'required' => true,
            'type' => 'string',
            'format' => 'email',
            'sanitize_callback' => 'sanitize_email',
            'validate_callback' => fn($v) => is_email($v) !== false,
        ],
        'username' => [
            'required' => true,
            'type' => 'string',
            'sanitize_callback' => 'sanitize_user',
            'validate_callback' => fn($v) => strlen($v) >= 3,
        ],
    ]);

SQL Injection Prevention

Never concatenate user input in queries:

// WRONG - SQL injection risk
$wpdb->query("SELECT * FROM posts WHERE id = " . $_GET['id']);

// CORRECT - Use prepared statements
$wpdb->prepare("SELECT * FROM posts WHERE id = %d", absint($_GET['id']));

CSRF Protection

AJAX routes handle CSRF via nonces. For frontend forms:

// In your form
<?php wp_nonce_field('myplugin_contact_form', '_wpnonce'); ?>

// In your controller
if (!wp_verify_nonce($_POST['_wpnonce'], 'myplugin_contact_form')) {
    wp_die('Security check failed');
}

Rate Limiting

Implement rate limiting for public endpoints:

class RateLimitMiddleware
{
    public function handle($request, callable $next): mixed
    {
        $ip = $_SERVER['REMOTE_ADDR'];
        $key = 'rate_limit_' . md5($ip . $_SERVER['REQUEST_URI']);
        $limit = 60; // requests per minute

        $count = (int) get_transient($key);

        if ($count >= $limit) {
            return new \WP_Error('rate_limit', 'Too many requests. Try again later.', ['status' => 429]);
        }

        set_transient($key, $count + 1, MINUTE_IN_SECONDS);

        return $next($request);
    }
}

Security Headers

Set appropriate headers for REST responses:

class SecurityHeadersMiddleware
{
    public function handle($request, callable $next): mixed
    {
        $response = $next($request);

        // Prevent content sniffing
        header('X-Content-Type-Options: nosniff');

        // Prevent clickjacking
        header('X-Frame-Options: DENY');

        return $response;
    }
}

Best Practices Summary

PracticeImplementation
Verify noncesDefault for AJAX routes
Check permissions->permission() or ->setPermissionCallback()
Sanitize inputsanitize_*() functions
Validate input->args() with validate_callback
Escape outputesc_html(), esc_attr(), etc.
Prepared queries$wpdb->prepare()
Rate limitingCustom middleware