WordPress Best Practices

A list of suggestions for developing WordPress sites on Pantheon.

Discuss in our Forum Discuss in Slack

This article provides suggestions, tips, and best practices for developing and managing WordPress sites on the Pantheon platform.

Development

  • We recommend using an IDE, or a text editor designed for development like Atom, Sublime Text, or Brackets.

  • Do not modify core WordPress files as it can cause unintended consequences, and can prevent you from updating your site regularly. If you need to modify any WP functionality, do it as a custom or Must Use plugin, which adheres to the WP.org Plugin best practices.

  • Use Redis. Redis is an open-source, networked, in-memory, key-value data store that can be used as a drop-in caching backend for your WordPress site. Pantheon makes it super simple and you'll be able to cache a lot of database queries in WordPress.

  • Use wp-cfm. It lets you store settings from the wp_options table in Git and pull it into the database. A lot of WordPress stuff is option-heavy and you can spend a lot of time trying to figure out what you missed between environments. This is true for all WordPress sites, but especially helpful on Pantheon where you have at least three environments you will need to reconfigure every time.

  • Use Grunt or Gulp to aggregate JS/CSS on your local development environment rather than relying on the server to do it for you. This helps speed up your workflow by minimizing redundant tasks.

  • When developing custom plugins or themes, it is best to abide by the WordPress Coding Standards for efficiency and ease of collaboration.

Plugins

  • Add Composer and pull your WordPress plugins from wpackagist.org. WordPress Packagist mirrors the WordPress.org plugin repository and adds a composer.json file so things play nice. It makes future debugging much simpler should you need to switch between multiple plugin or WordPress versions to see what caused something to break. While committing Composer dependencies is generally not recommended, you will have to commit the dependencies that Composer downloads on Pantheon since running composer install on the environments is not supported (just as Git submodules are not supported).

  • If you have a custom plugin that retrieves a specific post (or posts), instead of using wp_query() to retrieve it, use the get_post() function. While wp_query has its uses, the get_post function is built specifically to retrieve a WordPress Post object, and does so very efficiently.

  • Don't use plugins that create files vital to your site logic that you aren't willing to track in Git. Sometimes they're dumped in uploads, sometimes not, and you'll likely have difficulty trying to figure it out later. You'd be surprised how many uploads-type plugins rely on .htaccess files — avoid those as well.

Themes

  • In your theme, use a simple PHP include() instead of WordPress's get_template_part(). The overhead is heavy if your use case is simply adding in another sub-template file. For example:

    <?php get_template_part('content', 'sidebar'); ?>
    <?php include('content-sidebar.php'); ?>

Manage License Keys for Themes or Plugins

There are many plugins and themes in WordPress that require license keys. Since Dev and Multidev are the only writable environments in SFTP mode, it is best practice to associate the license key in a domain so you can easily update and deploy the updates to Test and Live environments.

Testing

  • Run Launch Check to review errors and get recommendations on your site's configurations.

  • Automate testing with Behat. Adding automated testing into your development workflow will help you deliver higher quality WordPress sites.

Live

Avoid XML-RPC Attacks

The /xmlrpc.php script is a potential security risk for WordPress sites. It can be used by bad actors to brute force administrative usernames and passwords, for example. This can be surfaced by reviewing your site's nginx-access.log for the Live environment. If you leverage GoAccess, you might see something similar to the following:

2 - Top requests (URLs)                                  Total: 366/254431

Hits Vis.     %   Bandwidth Avg. T.S. Cum. T.S. Max. T.S. Data
---- ---- ----- ----------- --------- --------- --------- ----
2026   48 0.77%   34.15 KiB   1.27  s  42.74 mn  38.01  s /xmlrpc.php
566   225 0.21%   12.81 MiB   4.08  s  38.45 mn  59.61  s /
262    79 0.10%  993.71 KiB   2.32  s  10.14 mn  59.03  s /wp-login.php

Pantheon recommends disabling XML-RPC, given the WordPress Rest API is a stronger and more secure method for interacting with WordPress via external services.

Pantheon blocked requests to xmlrpc.php by default in the WordPress 5.4.2 core release. If your version of WordPress is older than this, you can block xmlrpc.php attacks by applying your upstream updates.

Enable XML-RPC via Pantheon.yml

 Note

XML-RPC is not recommended on the Pantheon platform. Pantheon does not support XML-RPC if it is enabled.

You can re-enable access to XML-RPC for tools and plugins that require it, such as Jetpack or the WordPress mobile app.

  1. Modify your site's pantheon.yml file to allow access to the xmlrpc.php path:

    pantheon.yml
    protected_web_paths_override: true
    protected_web_paths:
      - /private
      - /wp-content/uploads/private

    This will maintain the normal security settings for other paths, but allows access for XMLRPC. Follow the remaining steps below to block all requests to the xmlrpc.php file EXCEPT those added to your IP address allowlist.

  2. Add Jetpack IP addresses to the is_from_trusted_ip function of your wp-config.php file.

  3. Change your disallow_uri array to:

    $disallow_uri = array(
            '/wp-login.php',
            '/wp-admin/',
            '/xmlrpc.php',
        ); 

Disable XML-RPC via a Custom Plugin

This method has the advantage of being toggleable without deploying code, by activating or deactivating a custom plugin. The result of creating and activating this plugin is that exploitable XMLRPC methods will no longer be available via POST requests.

  1. Set the connection mode to SFTP for the Dev or target Multidev environment via the Pantheon Dashboard or with Terminus:

    terminus connection:set <site>.<env> sftp
  2. Use Terminus and WP-CLI's scaffold plugin command to create a new custom plugin.

    In the following example, replace my-site with your Pantheon site name, and disable-xmlrpc with your preferred name for this new plugin:

    terminus wp my-site.dev -- scaffold plugin disable-xmlrpc
  3. Add the following lines to the main PHP plugin file:

    wp-content/plugins/disable-xmlrpc/disable-xmlrpc.php
    # Disable /xmlrpc.php
    add_filter('xmlrpc_methods', function () {
      return [];
    }, PHP_INT_MAX);

    If your site uses a nested web root directory, you must include that directory in the path. For example, if your nested web root is /wp, use /wp/xmlrpc.php instead of /xmlrpc.php

  4. Activate the new plugin from within the WordPress admin dashboard, or via Terminus and WP-CLI:

    terminus wp my-site.dev -- plugin activate disable-xmlrpc
  5. Commit your work, deploy code changes then activate the plugin on Test and Live environments.

Avoid WordPress Login Attacks

Similar to XML-RPC, the wp-login.php path can be subject to abuse by bots or other spammers. Unlike XML-RPC, which is no longer used often, wp-login.php is the primary WordPress login.

There are a few recommended actions you can take to protect yourself against login abuse.

Change the Admin Account Name

We strongly recommend that you change your admin account name. Many attacks assume the default name, “admin.” The easiest way to do this is to create a new user with administrator rights, log in with the new username, then delete the admin user.

Change the wp-login.php Path

Use a plugin like WPS Hide Login to change the login path from wp-login.php to any path you choose, such as /login or /admin. Then redirect all traffic from wp-login.php to the homepage or to another page like a 404.

Enforce Complex Passwords

WordPress suggests password complexity guidelines when you create a user and password, but it does not enforce password rules. Use a plugin like Better Passwords to set a minimum password length and alert users if they try to use a password that has been collected in a known data breach.

Disable "Anyone Can Register"

Some attackers or lost visitors might try to create an account via the login page. To disable this, navigate to the Settings tab in WordPress admin and uncheck Anyone can register on the Membership line.

Add Multi-factor Authentication (MFA)

Two Factor Authentication (2FA) and Multi-factor Authentication (MFA) are added layers of protection to ensure the security of your accounts beyond just a username and password. Multi-factor refers to the capability to have more than two factors of authentication (for example: password, SMS, and email verification). Use one of the many Two-Factor Authentication plugins to protect logins to your WordPress site.

Use Single Sign-On (SSO)

If your workspace makes use of an Identity Provider (IdP) such as Google Workspace, Microsoft AzureAD, or others for Single Sign-On, utilize that as the login authority for your WordPress site.

Some plugins or services can simplify the SSO integration of your IdP, such as WP SAML Auth or MiniOrange.

SSO often includes or requires MFA as well.

Disable Anonymous Access to WordPress Rest API

The WordPress REST API is enabled for all users by default. To improve the security of a WordPress site, you can disable the WordPress REST API for anonymous requests, to avoid exposing admin users. This action improves site safety and reduces unexpected errors that can result in compromised WordPress core functionalities.

The following function ensures that anonymous access to your site's REST API is disabled and that only authenticated requests will work. You can add this code sample to a theme's functions.php file or to a must-use plugin:

// Disable WP Users REST API for non-authenticated users (allows anyone to see username list at /wp-json/wp/v2/users)
add_filter( 'rest_authentication_errors', function( $result ) {
	if ( true === $result || is_wp_error( $result ) ) {
		return $result;
	}

	if ( ! is_user_logged_in() ) {
		return new WP_Error(
			'rest_not_logged_in',
			__( 'You are not currently logged in.' ),
			array( 'status' => 401 )
		);
	}

	return $result;
});

Security Headers

Pantheon's Nginx configuration cannot be modified to add security headers, and many solutions (including plugins) written about security headers for WordPress involve modifying the .htaccess file for Apache-based platforms.

There are plugins for WordPress that do not require .htaccess to set security headers, but header specifications may change more rapidly than the plugins can keep up with. In those cases, you may want to define the headers yourself.

Adding code like the example below in a plugin (or mu-plugin) can help add security headers for WordPress sites on Pantheon, or any other Nginx-based platform. Do not add this to your theme's functions.php file, as it will not be executed for calls to the REST API.

The code below is only an example to get you started. You'll need to modify it to match your needs, especially the Content Security Policy. Tools like SecurityHeaders.com can help to check your security headers, and link to additional information on how to improve your security header profile.

function additional_securityheaders( $headers ) {
  if ( ! is_admin() ) {
    $headers['Referrer-Policy']             = 'no-referrer-when-downgrade'; //This is the default value, the same as if it were not set.
    $headers['X-Content-Type-Options']      = 'nosniff';
    $headers['X-XSS-Protection']            = '1; mode=block';
    $headers['Permissions-Policy']          = 'geolocation=(self "https://example.com") microphone=() camera=()';
    $headers['Content-Security-Policy']     = "script-src 'self'";
    $headers['X-Frame-Options']             = 'SAMEORIGIN';
  }

  return $headers;
}
add_filter( 'wp_headers', 'additional_securityheaders' );

Note: Because the headers are applied by PHP code when WordPress is invoked, they will not be added when directly accessing assets like https://example.com/wp-content/uploads/2020/01/sample.json.