Skip to content

WordPress Security and Hardening

WordPress security in CorePanel works on two levels that complement each other:

  • Hardening — changes applied inside WordPress: constants in wp-config.php and a companion must-use plugin.
  • The WAF — rules applied in front of WordPress, blocking the request before PHP ever runs. See Web Application Firewall.

Hardening removes attack surface; the WAF stops attackers from reaching what is left.

Each is independent and reversible. On always means “protection enabled”, so xmlrpc = on means XML-RPC is blocked.

SwitchWhat it doesDefault on new installs
File editorDisables the plugin/theme editor in wp-admin (DISALLOW_FILE_EDIT)On
XML-RPCBlocks XML-RPC requestsOn
Version hidingRemoves the WordPress version from the page source and feedsOn
User enumerationBlocks ?author=N and REST endpoints that list usernamesOn
Application passwordsDisables WordPress application passwordsOff

Set them per instance from the Security view, or:

Terminal window
corepanel wp hardening 12 3 --set xmlrpc=on --set user-enumeration=on
  • File editor off. A stolen administrator session should not be a shell. This is the single highest-value switch and it costs a site nothing: plugin and theme files should come from an update or a deploy, not from a textarea in a browser.
  • XML-RPC blocked. Modern WordPress uses the REST API. XML-RPC remains a favourite for password-guessing amplification (system.multicall) and pingback abuse. Turn it back on only if something you actually use needs it — some older mobile apps and the Jetpack connection do.
  • Version hidden. Not a defence in itself, but it removes the free targeting hint.
  • User enumeration blocked. Usernames are half of a credential-stuffing attempt. There is no reason for a public endpoint to hand out the login names of your administrators.
  • Application passwords left alone. They are a legitimate integration mechanism, so disabling them is offered but not imposed.

The switches that live inside WordPress are applied by a must-use plugin CorePanel installs at wp-content/mu-plugins/corepanel.php. Besides hardening, it produces the one-time login links used by Log in in the panel, and it tells the server what may be page-cached.

The panel owns the file. It is rewritten from scratch on every hardening or login operation, so edits made by hand are lost at the next one. The supported extension points are the filters below, not the file.

Turning it off. A must-use plugin cannot be deactivated from wp-admin, so there are two escapes, both constants in wp-config.php:

ConstantEffect
define( 'COREPANEL_CACHE_OFF', true );Disables only the cache module. Hardening and login links keep working.
define( 'COREPANEL_MU_OFF', true );Disables the companion entirely — the file returns before doing anything.

Use the first if a theme misbehaves under caching, and the second only to rule the companion out while diagnosing something else.

This section is the contract the module speaks; enabling, measuring and troubleshooting the cache itself is in Speed Optimizations.

It stores nothing. On each anonymous page view it decides whether the HTML just rendered is the same HTML any other visitor without state would get, and declares that to the server, which owns the actual cache. The module does nothing at all unless the dynamic cache is enabled for that domain — with the cache off it reads one environment variable that is not there and registers no hooks.

Pages it never declares cacheable: wp-admin, REST and AJAX, anything but a GET, logged-in visitors, searches, previews, password-protected posts, visitors who have left a comment, 404s, the WooCommerce cart, checkout and account pages, and any request that filled a cart or started a PHP session while rendering.

Sites whose consent banner renders from the cookie. A banner that decides in PHP what the page contains — which tracking tags go in it, or a cookies-accepted class on <body> — turns one URL into two different pages, and the cache stores one copy. Whoever loads it first decides what every later visitor gets, and one of the two directions serves tracking tags to people who refused them. The module recognises Cookie Notice & Compliance and CookieYes in its legacy mode, and declares those sites uncacheable rather than get it wrong.

Most banners are not affected. Complianz, Real Cookie Banner and CookieYes connected to its web app rewrite the page identically for every visitor and unblock in the browser, so their HTML is shareable and they cache normally.

For any other banner — or a theme with its own if ( $_COOKIE[…] ) — the check takes a minute. Fetch the page twice, with and without the consent cookie, and compare:

Terminal window
curl -s 'https://example.com/?cache-check=1' > without.html
curl -s 'https://example.com/?cache-check=2' \
-H 'Cookie: your-consent-cookie=yes' > with.html
diff without.html with.html

The throwaway parameter is what makes each request new to the cache, so both reach WordPress and you compare what it renders rather than two copies of one stored page. A request header asking not to cache would not do it — visitors do not get to empty a shared cache, so the server ignores it.

Differences beyond nonces, timestamps and the parameter itself mean the page depends on consent; use the corepanel_cache_public filter below to keep those pages out of the cache.

Whatever the module declares, the server keeps a veto of its own: a response that issues a cookie is never stored, and neither is one whose own Cache-Control says private or no-store. A page cache that trusted the application and nothing else would be one plugin bug away from serving a logged-in visitor’s page to everybody.

The same applies to Vary. A response declaring that it varies on something the cache does not distinguish — Vary: Cookie is the common one, emitted indiscriminately by a number of plugins — is not stored, because a single stored copy would then answer every visitor whatever they sent. If a site caches far less than expected, an over-eager Vary from a plugin is worth ruling out first.

Two filters let a theme or plugin override the decision:

// Never cache this page.
add_filter( 'corepanel_cache_public', '__return_false' );
// Cache it, but for an hour instead of the default.
add_filter( 'corepanel_cache_max_age', function () { return HOUR_IN_SECONDS; } );

The declared lifetime is also capped automatically whenever the page mints a WordPress nonce, so a cached page can never outlive the nonce inside it — a comment form that silently stops accepting submissions is the classic symptom of a page cache that ignores this, and it produces no error anywhere.

Publishing, editing, trashing or commenting on a post invalidates the URLs it affects — the post itself, the front page, its category and tag archives, the feed and the sitemaps. Changing the theme, the menus, the permalink structure or activating a plugin invalidates the whole site.

In front of all this, the WAF ships a WordPress-specific rule family that stops attacks before PHP runs:

RuleWhat it catches
WPE-001wp-login brute force from non-browser clients
WPE-002XML-RPC amplification (multicall / pingback)
WPE-010 / WPE-011User enumeration via ?author=N and via the REST API
WPE-020 / WPE-022Access to sensitive config, backup and debug.log files
WPE-030WPE-032Path traversal in the URI, query and POST arguments
WPE-040 / WPE-041Executable uploads via double extensions and server control files
WPE-050 / WPE-051Comment spam and XSS in comment bodies

The extended WordPress package (WPE) is available on every edition, including Personal. It is off by default rather than on, because several of these rules touch flows a plugin or a mobile app may legitimately use — turn it on under WAF → Overview → Protection packages, in detect first if the site is busy. The core WordPress rules (WP-001WP-004) are part of the baseline protection and always active.

Hardening and WAF rules overlap on purpose — user enumeration, for example, is blocked both inside WordPress and at the edge. Whichever layer sees the request first wins, and neither depends on the other being enabled.

If a site has been hacked, do these in order:

  1. Maintenance mode on — stop serving the compromised site (toggles).
  2. Integrity check — find modified core files.
  3. Reinstall core — replace core files, keeping wp-content and wp-config.php.
  4. Review plugins and themes — remove anything unused, update everything, and look hard at anything not from the official directory.
  5. Look through uploads for PHP files. Nothing in wp-content/uploads should execute.
  6. Rotate credentials — WordPress administrators, the database user, and the account’s FTP password.
  7. Enable the hardening switches, and check the WAF’s event log for how they got in.
  8. Maintenance mode off.

A restore point or an account backup from before the compromise is faster than any of this — if one exists, and if you are sure of the date.