WordPress Security

WordPress Security for Headless and Decoupled Sites: API Protection

Secure headless WordPress implementations with API authentication, CORS configuration, rate limiting, and frontend protection strategies.

S
Sarah Chen
9 min read
2,094 views
Security guide for headless WordPress implementations

Headless WordPress architectures introduce unique security considerations. When WordPress serves only as a backend API, traditional security measures must be adapted and new protections implemented.

Headless Security Challenges

Decoupled architectures face specific risks:

  • API endpoint exposure
  • Cross-origin request handling
  • Authentication token management
  • Data over-exposure in responses
  • Rate limiting across distributed frontends

API Authentication

Implement secure API authentication:

// JWT authentication for headless WordPress
function generate_jwt_token($user_id) {
    $secret = defined('JWT_SECRET') ? JWT_SECRET : wp_salt('auth');
    $issued_at = time();
    $expiration = $issued_at + (HOUR_IN_SECONDS * 24); // 24 hours

    $payload = array(
        'iss' => home_url(),
        'iat' => $issued_at,
        'exp' => $expiration,
        'user_id' => $user_id,
        'roles' => get_user_roles($user_id),
    );

    $header = base64_encode(json_encode(array('typ' => 'JWT', 'alg' => 'HS256')));
    $payload_encoded = base64_encode(json_encode($payload));
    $signature = hash_hmac('sha256', $header . '.' . $payload_encoded, $secret);

    return $header . '.' . $payload_encoded . '.' . base64_encode($signature);
}

function verify_jwt_token($token) {
    $secret = defined('JWT_SECRET') ? JWT_SECRET : wp_salt('auth');

    $parts = explode('.', $token);
    if (count($parts) !== 3) {
        return false;
    }

    list($header, $payload, $signature) = $parts;

    // Verify signature
    $expected_signature = base64_encode(
        hash_hmac('sha256', $header . '.' . $payload, $secret)
    );

    if (!hash_equals($expected_signature, $signature)) {
        return false;
    }

    // Decode and verify payload
    $payload_data = json_decode(base64_decode($payload), true);

    if ($payload_data['exp'] < time()) {
        return false; // Token expired
    }

    return $payload_data;
}

// Authentication endpoint
add_action('rest_api_init', function() {
    register_rest_route('auth/v1', '/token', array(
        'methods' => 'POST',
        'callback' => 'handle_token_request',
        'permission_callback' => '__return_true',
    ));
});

function handle_token_request($request) {
    $username = sanitize_user($request->get_param('username'));
    $password = $request->get_param('password');

    $user = wp_authenticate($username, $password);

    if (is_wp_error($user)) {
        return new WP_Error('invalid_credentials', 'Invalid credentials', array('status' => 401));
    }

    $token = generate_jwt_token($user->ID);
    $refresh_token = generate_refresh_token($user->ID);

    return array(
        'token' => $token,
        'refresh_token' => $refresh_token,
        'expires_in' => HOUR_IN_SECONDS * 24,
    );
}

CORS Configuration

Properly configure cross-origin requests:

function configure_cors_headers() {
    $allowed_origins = array(
        'https://frontend.example.com',
        'https://www.example.com',
        'https://staging.example.com',
    );

    $origin = $_SERVER['HTTP_ORIGIN'] ?? '';

    if (in_array($origin, $allowed_origins)) {
        header('Access-Control-Allow-Origin: ' . $origin);
        header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
        header('Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With');
        header('Access-Control-Allow-Credentials: true');
        header('Access-Control-Max-Age: 86400'); // 24 hours
    }

    // Handle preflight requests
    if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
        http_response_code(200);
        exit;
    }
}
add_action('rest_api_init', 'configure_cors_headers', 1);

// Block requests from unauthorized origins
add_filter('rest_pre_dispatch', function($result, $server, $request) {
    $allowed_origins = array(
        'https://frontend.example.com',
        'https://www.example.com',
    );

    $origin = $_SERVER['HTTP_ORIGIN'] ?? '';

    // Allow requests without origin (server-to-server)
    if (empty($origin)) {
        return $result;
    }

    if (!in_array($origin, $allowed_origins)) {
        return new WP_Error(
            'cors_forbidden',
            'Origin not allowed',
            array('status' => 403)
        );
    }

    return $result;
}, 10, 3);

API Rate Limiting

Implement rate limiting for API endpoints:

function apply_api_rate_limit($result, $server, $request) {
    $route = $request->get_route();

    // Different limits for different endpoints
    $rate_limits = array(
        '/wp/v2/posts' => array('limit' => 100, 'window' => 60),
        '/wp/v2/users' => array('limit' => 20, 'window' => 60),
        '/auth/v1/token' => array('limit' => 5, 'window' => 60),
        'default' => array('limit' => 60, 'window' => 60),
    );

    $config = $rate_limits[$route] ?? $rate_limits['default'];

    // Get identifier (API key, user ID, or IP)
    $identifier = get_api_identifier($request);
    $cache_key = 'api_rate_' . md5($route . $identifier);

    $current = get_transient($cache_key) ?: 0;

    if ($current >= $config['limit']) {
        return new WP_Error(
            'rate_limit_exceeded',
            'Rate limit exceeded',
            array(
                'status' => 429,
                'headers' => array(
                    'X-RateLimit-Limit' => $config['limit'],
                    'X-RateLimit-Remaining' => 0,
                    'Retry-After' => $config['window'],
                ),
            )
        );
    }

    set_transient($cache_key, $current + 1, $config['window']);

    // Add rate limit headers to response
    add_filter('rest_post_dispatch', function($response) use ($config, $current) {
        $response->header('X-RateLimit-Limit', $config['limit']);
        $response->header('X-RateLimit-Remaining', $config['limit'] - $current - 1);
        return $response;
    });

    return $result;
}
add_filter('rest_pre_dispatch', 'apply_api_rate_limit', 10, 3);

Response Data Filtering

Prevent data over-exposure in API responses:

// Remove sensitive fields from user responses
add_filter('rest_prepare_user', function($response, $user, $request) {
    $data = $response->get_data();

    // Remove fields that should not be exposed
    unset($data['registered_date']);
    unset($data['capabilities']);
    unset($data['extra_capabilities']);
    unset($data['avatar_urls']);

    // Only include email for authenticated requests
    if (!is_user_logged_in() || get_current_user_id() !== $user->ID) {
        unset($data['email']);
    }

    $response->set_data($data);
    return $response;
}, 10, 3);

// Limit post fields in listing endpoints
add_filter('rest_prepare_post', function($response, $post, $request) {
    // Check if this is a listing request
    if ($request->get_param('per_page') !== null) {
        $data = $response->get_data();

        // Remove heavy fields from listings
        unset($data['content']['raw']);
        unset($data['excerpt']['raw']);

        $response->set_data($data);
    }

    return $response;
}, 10, 3);

Disabling Unnecessary Endpoints

Remove endpoints you do not need:

add_filter('rest_endpoints', function($endpoints) {
    // Remove user enumeration
    unset($endpoints['/wp/v2/users']);
    unset($endpoints['/wp/v2/users/(?P[\d]+)']);

    // Remove comment endpoints if not needed
    unset($endpoints['/wp/v2/comments']);

    // Remove settings endpoint
    unset($endpoints['/wp/v2/settings']);

    // Remove block types
    unset($endpoints['/wp/v2/block-types']);

    return $endpoints;
});

Frontend Security Headers

Add security headers for API responses:

add_filter('rest_post_dispatch', function($response) {
    $response->header('X-Content-Type-Options', 'nosniff');
    $response->header('X-Frame-Options', 'DENY');
    $response->header('Cache-Control', 'no-store, private');

    // Content Security Policy for JSON responses
    $response->header(
        'Content-Security-Policy',
        "default-src 'none'; frame-ancestors 'none'"
    );

    return $response;
});

Webhook Security

Secure outgoing webhooks to frontend:

function send_secure_webhook($event, $data, $endpoint) {
    $timestamp = time();
    $payload = json_encode(array(
        'event' => $event,
        'data' => $data,
        'timestamp' => $timestamp,
    ));

    // Sign the webhook
    $secret = get_option('webhook_secret');
    $signature = hash_hmac('sha256', $payload, $secret);

    $response = wp_remote_post($endpoint, array(
        'body' => $payload,
        'headers' => array(
            'Content-Type' => 'application/json',
            'X-Webhook-Signature' => $signature,
            'X-Webhook-Timestamp' => $timestamp,
        ),
        'timeout' => 10,
    ));

    // Log webhook delivery
    log_webhook_delivery($event, $endpoint, !is_wp_error($response));

    return $response;
}

Conclusion

Headless WordPress security requires proper API authentication, CORS configuration, rate limiting, and response filtering. The separation of frontend and backend introduces new attack vectors that need specific protection strategies.

Share:
S
Written by Sarah Chen

WP Folder Shield Team

Related Articles

SEO Spam Injection: How to Detect Hidden Links and Malicious Redirects
SEO Spam Injection: How to Detect Hidden Links and Malicious Redirects

Learn how hackers inject hidden links and malicious redirects into WordPress sites to steal your...

January 18, 2026
Understanding WordPress Malware Signatures and Detection Patterns
Understanding WordPress Malware Signatures and Detection Patterns

Learn how malware scanners detect threats using signatures and patterns. Understand the technology...

January 15, 2026
Country Blocking for WooCommerce: Protect Your Online Store
Country Blocking for WooCommerce: Protect Your Online Store

Learn how to implement country blocking for WooCommerce stores. Prevent fraud, reduce chargebacks...

January 10, 2026

Ready to Secure Your WordPress Site?

Get complete protection with WP Folder Shield.

Get Started