Reduced attack surface: the structural advantage of headless

In a traditional architecture, WordPress simultaneously exposes its back-office, its PHP themes, and its plugins to the public. In headless mode, the WordPress theme is no longer accessible to visitors. Only the API (REST or GraphQL) is exposed, which significantly reduces the number of attack vectors.

This separation does not relieve you of securing each layer individually. The following sections detail the measures to apply to the WordPress back-end, the API, and the Next.js frontend.

Securing the WordPress back-office

Restrict access to wp-admin

The WordPress admin panel must only be accessible to authorized IP addresses. This restriction is applied at the web server level (Apache or Nginx).

# Nginx — restrict wp-admin access by IP
location /wp-admin {
  allow 203.0.113.10;   # Office IP
  allow 198.51.100.0/24; # VPN range
  deny all;
}

location /wp-login.php {
  allow 203.0.113.10;
  allow 198.51.100.0/24;
  deny all;
}

Two-factor authentication (2FA)

Install a 2FA plugin (WP 2FA, Two Factor Authentication) to require a temporary code in addition to the password. This measure neutralizes brute-force attacks and password compromises.

Limit login attempts

The Limit Login Attempts Reloaded plugin blocks IP addresses after a configurable number of failed login attempts. Recommended settings: 3 attempts before a 15-minute lockout, then progressive blocking.

Restrict allowed IPs on wp-admin

Configure your web server (Nginx or Apache) to allow only IP addresses from your team or VPN. Any access attempt from an unauthorized IP receives a 403 error.

Enable 2FA

Install a 2FA plugin and configure it for every account with the editor role or higher. Prefer TOTP applications (Google Authenticator, Authy) over SMS codes.

Limit login attempts

Install Limit Login Attempts Reloaded and configure a threshold of 3 attempts. Progressive lockouts deter automated attacks.

Disable XML-RPC

The XML-RPC protocol is a vector for brute-force and DDoS attacks. In headless mode, it has no use. Disable it in the .htaccess file or via a plugin.

Disable XML-RPC

The xmlrpc.php file allows remote connections to WordPress. In a headless architecture, it is unused and constitutes an attack vector (brute force, DDoS via pingback). Disable it:

// functions.php — disable XML-RPC
add_filter('xmlrpc_enabled', '__return_false');
# Nginx — block xmlrpc.php at the server level
location = /xmlrpc.php {
  deny all;
}

Hide the WordPress version

The WordPress version is exposed by default in the HTML source code and in HTTP headers. This information makes it easier to target known exploits.

// functions.php — remove WordPress version
remove_action('wp_head', 'wp_generator');
add_filter('the_generator', '__return_empty_string');

Securing the API

Authentication of sensitive endpoints

The WordPress REST API is partially public by default. Read endpoints (posts, pages) are accessible without authentication, which is necessary for the frontend. Write endpoints and sensitive data, however, must require authentication.

// Restrict API access to authenticated users for certain endpoints
add_filter('rest_authentication_errors', function ($result) {
  if (!empty($result)) {
    return $result;
  }
  if (!is_user_logged_in()) {
    // Allow only public endpoints
    $public_routes = ['/wp/v2/posts', '/wp/v2/pages', '/wp/v2/categories'];
    $current_route = $_SERVER['REQUEST_URI'];
    // Block non-public endpoints
  }
  return $result;
});

Rate limiting

Rate limiting caps the number of API requests per unit of time and per IP address. Without this protection, an attacker can overload the server or massively extract data.

# Nginx — rate limiting on the REST API
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;

location /wp-json/ {
  limit_req zone=api burst=10 nodelay;
  proxy_pass http://wordpress_backend;
}

CORS configuration

CORS (Cross-Origin Resource Sharing) headers control which domains can query the API. Limit allowed origins to your frontend domain.

// functions.php — restrict CORS to your frontend
add_action('rest_api_init', function () {
  remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
  add_filter('rest_pre_serve_request', function ($value) {
    header('Access-Control-Allow-Origin: https://www.your-site.com');
    header('Access-Control-Allow-Methods: GET, OPTIONS');
    header('Access-Control-Allow-Headers: Authorization, Content-Type');
    return $value;
  });
});

API key for the frontend

For public endpoints, use an API key passed via a custom header (for example X-API-Key). This key is not a strong security mechanism (it's visible in client code) but it allows you to filter out illegitimate requests and to make per-client rate limiting easier.

Hardening wp-config.php

The wp-config.php file contains the database credentials and security keys. Apply these measures:

// wp-config.php — hardening measures

// Disable the file editor in admin
define('DISALLOW_FILE_EDIT', true);

// Force HTTPS for admin
define('FORCE_SSL_ADMIN', true);

// Limit post revisions (reduce data surface)
define('WP_POST_REVISIONS', 5);

// Disable debug mode in production
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', false);

Environment variables and secret management

Secrets (API keys, tokens, database credentials) must never appear in versioned source code.

Hard rule: no secrets in code

Use environment variables to store secrets. On the Next.js side, variables prefixed with NEXT_PUBLIC_ are exposed to the browser. WordPress API keys, tokens, and database credentials must use variables without that prefix, accessible only on the server side.

# .env.local (Next.js) — never committed to Git
WORDPRESS_API_URL=https://admin.your-site.com/wp-json
WORDPRESS_AUTH_TOKEN=your_secret_token
REVALIDATION_SECRET=your_isr_secret

# These variables are accessible only on the server
# DO NOT use NEXT_PUBLIC_ for secrets

Security headers on the Next.js frontend

The next.config.js file lets you set HTTP security headers applied to every response from the frontend.

// next.config.js — security headers
const securityHeaders = [
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-XSS-Protection', value: '1; mode=block' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' https://admin.your-site.com data:;"
  },
  { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
];

module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }];
  },
};

HTTPS and DDoS protection

HTTPS everywhere

TLS encryption is mandatory on both layers:

  • Frontend: certificate managed automatically by Vercel, Netlify, or your host
  • WordPress backend: Let's Encrypt certificate or one provided by the host, with HTTP to HTTPS redirection

DDoS protection via CDN

A Next.js frontend deployed on Vercel or Netlify benefits natively from CDN DDoS protection. For the WordPress backend, place it behind Cloudflare or an equivalent service.

Dependency auditing

The Next.js project's npm dependencies may contain known vulnerabilities. Integrate auditing into your CI/CD pipeline.

# Check dependency vulnerabilities
npm audit

# Automatically fix compatible vulnerabilities
npm audit fix

# Continuous monitoring (GitHub Dependabot or Snyk)

Automated audit

Enable GitHub Dependabot or Snyk on your repository. These tools detect vulnerabilities in dependencies and propose automatic updates via pull requests.