Skip to content

Hardening

Arkazzae edited this page Oct 7, 2026 · 1 revision

Hardening switches on a set of safe defaults that do not change how the site works for visitors: no code editor in wp-admin, no username discovery, no admin-level role for new accounts, one generic login error, no WordPress version in the page source, XML-RPC off and basic security headers. Each part has its own switch, and a part that Wordfence or wp-config.php already provides is skipped.

Hardening deliberately leaves out a Content Security Policy (it breaks payment iframes unless it is tuned for each site), stripping ?ver= from asset addresses (it breaks cache busting) and any writes to .htaccess.

Module ID hardening
Default On
Available Always
WP-CLI None
Since 0.1.0

How it works

File editor

Disable the theme/plugin file editor takes the edit_plugins, edit_themes and edit_files capabilities away from every user, administrators included. WordPress then hides the theme and plugin file editors in wp-admin. Someone who steals an administrator session cannot paste PHP code into a plugin file from the browser.

Username discovery

Block username discovery closes four places where WordPress shows login names:

Where Who it applies to What happens
?author=N scans, such as https://example.com/?author=1 Visitors who are not logged in A 301 redirect to the home page, for any value of author
The REST API users endpoint (/wp-json/wp/v2/users) Visitors who are not logged in HTTP 401 with the code rest_user_cannot_view and the message "Sorry, you are not allowed to list users."
The users sitemap Everyone Removed from the WordPress sitemap
oEmbed data Everyone author_name and author_url are removed

The REST check matches the users controller itself, not the route text, so variants of the address (for example in other letter case) are blocked too. Logged-in users can still use the users endpoint.

The redirect runs before WordPress would turn ?author=1 into /author/<name>/, so a scan does not learn the name from the redirect either.

Default role for new accounts

Attackers who get write access to the database can set the default role for new accounts to Administrator and then register their own account. Keep new accounts out of admin-level roles stops that.

When something reads the default role, SafeHouse checks the stored role. If the role has any of these capabilities, new accounts get Subscriber instead:

manage_options, edit_users, create_users, promote_users, delete_users, install_plugins, activate_plugins, edit_plugins, edit_themes, edit_files, update_core, unfiltered_html, manage_woocommerce

Editor (which has unfiltered_html) and WooCommerce's Shop manager (which has manage_woocommerce) are on that list on purpose: raw HTML can carry scripts, and a Shop manager runs the shop. A role name that does not exist is passed through unchanged.

SafeHouse changes only what WordPress reads, never the stored value. In WP-CLI and on Settings → General WordPress sees the real stored role, so you can see and fix it there.

Registration check in Site Health

While Hardening runs, Tools → Site Health has the test "SafeHouse registration check". It reads the stored default role, past the filter above, and shows one of three results:

Result Status When
"New accounts are set to get a role that can manage the site" Critical The default role has one of the capabilities listed above. This shows even when registration is closed.
"Anyone can register an account" Recommended Anyone can register is on and the role is safe.
"Registration is closed" Good "Only administrators can create WordPress accounts."

The critical result explains: "Attackers set this so that they can register their own administrator. Change it in Settings → General and check who changed it." While Keep new accounts out of admin-level roles is on, it adds "Until then SafeHouse gives new accounts the Subscriber role." The recommended result points out that "WooCommerce customer accounts have their own setting and keep working." Both have the link Open general settings.

Generic login errors

WordPress tells a wrong username apart from a wrong password, which lets bots find real login names. With Generic login errors on, both get one message:

Error: The username, email address or password is incorrect.

This applies to every login that goes through WordPress's authenticate filter: wp-login.php, the WooCommerce My Account login form and XML-RPC. Only WordPress's errors for an unknown username, an unknown e-mail address and a wrong password (invalid_username, invalid_email, incorrect_password) are replaced. Every other error passes through unchanged, including the Login limits lockout and pause messages, the Bot protection messages and two-factor errors from other plugins.

WordPress version

Hide the WordPress version removes the generator meta tag from the page <head> and the generator line from feeds.

XML-RPC

With Disable XML-RPC on:

  • every request to xmlrpc.php gets HTTP 403 with the plain-text body XML-RPC is disabled on this site.
  • WordPress's XML-RPC switch is turned off, and the pingback methods are removed
  • the X-Pingback header and the RSD link in the page <head> are removed

The WordPress mobile app and old desktop editors that use XML-RPC stop working with the site. When Jetpack or WooPayments is active, which need XML-RPC, this part is skipped (see With Wordfence and other plugins).

Security headers

Basic security headers adds three response headers:

X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: SAMEORIGIN

HSTS (1 year, this domain only) adds one more, only on requests WordPress sees as HTTPS:

Strict-Transport-Security: max-age=31536000

The HSTS header has no includeSubDomains and no preload, so it applies to this host name only. Once a browser has seen it, that browser refuses plain HTTP to the host for a year, even if you turn the switch off later.

SafeHouse sends these headers from WordPress's send_headers action, which WordPress runs for front-end pages. Responses from wp-admin, wp-login.php and the REST API do not get them from SafeHouse. SafeHouse also skips a header when PHP has already set one with the same name, for example from another plugin.

Settings

On SafeHouse → Modules → Hardening, select Details. All settings are switches.

Setting Default What it does
Disable the theme/plugin file editor On Removes the file editors for everyone.
Block username discovery On "For visitors who are not logged in: ?author=N scans, the REST users endpoint, the users sitemap and author data in oEmbed."
Keep new accounts out of admin-level roles On "If the default role for new accounts can manage users, plugins, options, the shop or raw HTML (for example after a database attack), new accounts get Subscriber instead."
Generic login errors On "One message for a wrong username or a wrong password, on wp-login.php and the WooCommerce login form."
Hide the WordPress version On Removes the generator tag.
Disable XML-RPC On "xmlrpc.php answers 403. The WordPress mobile app and old desktop editors stop working with this site. Skipped automatically when Jetpack or WooPayments is active."
Basic security headers On "X-Content-Type-Options, Referrer-Policy and X-Frame-Options (same origin). Pages served from a full-page cache may not get them."
HSTS (1 year, this domain only) Off "Only on HTTPS. Browsers will refuse plain HTTP for a year, so turn it on only when HTTPS works everywhere on this domain."

Hardening has no panel and no buttons.

Fix a changed default role

If Site Health shows "New accounts are set to get a role that can manage the site":

  1. Go to Settings → General.

    This screen shows the real stored role, not the Subscriber role SafeHouse hands out in the meantime.

  2. Set the default role for new accounts to the role your site needs, usually Subscriber.

  3. Select Save Changes.

Then find out who changed it. If Change alerts runs, the activity log has the change: as option_changed when it was made through WordPress, or as inventory_changed when the hourly check found it, for example after a change straight in the database.

Turn on HSTS

Warning

For a year after a browser sees the HSTS header, that browser refuses plain HTTP to this host name. Turning the switch off does not undo that.

  1. Check that every page and every asset of the site loads over HTTPS on this host name.
  2. Go to SafeHouse → Modules → Hardening and select Details.
  3. Turn on HSTS (1 year, this domain only).
  4. Select Save Changes.

Behind a proxy or load balancer that ends HTTPS and talks to the server over plain HTTP, WordPress may not see the request as HTTPS. SafeHouse then does not send the header.

With Wordfence and other plugins

A part that something else already provides is locked on the settings page with the note "Handled by , skipped here.", and SafeHouse does not run it:

Setting Skipped when Note shown
Disable the theme/plugin file editor DISALLOW_FILE_EDIT or DISALLOW_FILE_MODS is set to true in wp-config.php "Handled by wp-config.php, skipped here."
Block username discovery Wordfence is active and its option to prevent username discovery is on "Handled by Wordfence, skipped here."
Generic login errors Wordfence is active, its option to mask login errors is on, and WooCommerce is not active "Handled by Wordfence, skipped here."
Hide the WordPress version Wordfence is active and its option to hide the WordPress version is on "Handled by Wordfence, skipped here."
Disable XML-RPC Jetpack is active "Handled by Jetpack (needs XML-RPC), skipped here."
Disable XML-RPC WooPayments is active "Handled by WooPayments (needs XML-RPC), skipped here."

With WooCommerce active, SafeHouse keeps its own generic login errors even when Wordfence masks them, because Wordfence covers wp-login.php only and the WooCommerce login form would still tell unknown users from wrong passwords.

Keep new accounts out of admin-level roles, Basic security headers and HSTS (1 year, this domain only) are never skipped.

Set SHOUSE_IGNORE_OVERLAPS to true in wp-config.php to run every part anyway. That also turns XML-RPC off while Jetpack or WooPayments is active, although they need it.

What it stores

Only its settings, in the shouse_settings option. Hardening writes no activity log entries and sends no e-mails.

Limitations

  • Only ?author= scans are redirected. Author archives at /author/<name>/ still exist and show the author's URL name, which is often the login name. Themes can also print author names on posts.
  • Editor and Shop manager count as admin-level roles. A site that wants new accounts to become Editors or Shop managers automatically must turn Keep new accounts out of admin-level roles off.
  • The security headers and HSTS are sent on front-end pages only, not on wp-admin, wp-login.php or REST API responses. Pages served from a full-page cache may not have them.
  • SafeHouse only sees headers set in PHP. If the web server configuration adds the same headers, a response can carry them twice; turn Basic security headers off in that case.
  • There is no Content Security Policy, no Permissions-Policy and no HSTS for subdomains.

Related pages

Clone this wiki locally