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
| Practice | Implementation |
|---|---|
| Verify nonces | Default for AJAX routes |
| Check permissions | ->permission() or ->setPermissionCallback() |
| Sanitize input | sanitize_*() functions |
| Validate input | ->args() with validate_callback |
| Escape output | esc_html(), esc_attr(), etc. |
| Prepared queries | $wpdb->prepare() |
| Rate limiting | Custom middleware |