WordPress.org

WordPress Developer Blog

Website security checks – WP-CLI for site owners and administrators

Website security checks – WP-CLI for site owners and administrators

If you are a website owner or administrator, one of many things you do is keep this website safe and protected. To achieve it, you need all the tools at your disposal to help you be productive and ensure you’re doing it right. A command-line interface for WordPress, WP-CLI, is one of those tools. If you think that’s too advanced for you and could never understand it, buckle up because this article is for you. Hopefully, you will feel confident enough to try some commands by the end of it. 

No installation needed

Installation of the WP-CLI tool is done via terminal and can be intimidating, especially because it varies depending on the operating system and server. We will look at ready-to-use options. Many hosting providers offer WP-CLI out of the box. Furthermore, many of them have their own custom commands to help you do some tasks specific to their servers. All of them have instructions on how to find and use WP-CLI on their platform. Search for WP-CLI in their knowledge base, documentation, blog, FAQs, etc.

It’s worth noting here that when you have WP-CLI available for your hosting, this means you have SSH access to your website. You don’t need to know anything about it, besides that, it is a very secure connection. In time, after  you get more comfortable with the terminal, you might want to learn how to do more than just WP-CLI. But it is completely fine if this never happens. This is the beauty of CLI tools – you can use it at as basic or as advanced a level as you want, without ever breaking anything.   

Agenda

Now that the heavy part of installing WP-CLI is completed, let’s see what we will cover. The first step in ensuring your website’s security is keeping WordPress core, plugins, and themes up to date. You also want to make sure these updates won’t break your website.

Another important way to remove stress is to prevent hacking. Occasionally, this cannot be done, but there are many things you can do in this area, such as monitoring file changes. Also, with WP-CLI, you have a powerful user management tool in your pocket.

Additional backups can be yet another provider of a good night’s sleep. 

It looks like we have our agenda

Anatomy of a command

Before we even start running commands, here are some quick basics that can make it easier to memorise commands. A WP-CLI command, almost always, has the following structure:

wp <noun> <verb> --flag-1 --flag-2

Noun and verb rarely switch order, but we won’t be looking at those commands now. Nouns are commands that represent entities you want to work with, such as core, plugin, theme, post, user etc. Verbs are a deeper level of subcommands which describe what you intend to do with this entity. You’ll find the same verbs across many entities, such as list, create, delete, add, remove etc. Flags are additional arguments to make your request more specific. 

Get help

At any level of the command structure (main, noun, or verb), you can use a global parameter named --help, which will reveal all subcommands and flags you can use for the current command or subcommand.

For example, if you want to see which subcommands (verbs) you can use with the plugin command, you can type:

wp plugin --help

If you want to know all the flags you can use with plugin update subcommand, you’ll type:

wp plugin update --help

To get out of the help screen, just type q and press Enter.

Are you in your WordPress’ root?

All the commands you’ll see in this article, are to be run inside the WordPress root folder. The WordPress root folder can be identified by the presence of following directories: /wp-admin, /wp-includes, and /wp-content.

When you log in to your hosting terminal, it is highly likely you won’t be in the WordPress root. This differs depending on the provider, but the most common scenario is that you are at least one level above.
To see in which directory you are, use the ls command.

In some cases, you’ll see public_html, in other www, maybe even the directory named the same as your username on the server. Your WordPress root is in one of those. To see what’s inside those directories, without going there, use the same command, followed by the name of the directory:

ls public_html/

Now that we have located WordPress root, let’s go there. Use the cd command:

cd public_html/

You can run ls command once again, just to be sure that you are, indeed, in WordPress root. This is the place we will stay in until the end of the article. Make yourself at home.

Secure updates

To make sure your WordPress core is secure, you want to keep it up to date with secure updates. Those updates are also called “minor.” 

There are two types of updates to the WordPress core software that become available for your website:

  • Major core updates that include new WordPress features. You can tell an update is considered a “major” release because the number will follow the pattern: 4.0, 4.1, 4.2, 4.3, 4.4, etc.
  • Minor core updates often include maintenance releases, security fixes, and updates to translation files. You can tell an update is considered a “minor” release because the number identifying it will be appended to the number for the current major release. For example: 4.4.1, 4.4.2, 4.4.3, etc.
– Source: Learn.WordPress.org, Managing Updates

For updating to a major release, you want to consult your developer. This has to be tested in a local environment to make sure everything works as expected with new features. 

Minor updates, however, you can perform yourself. First, check which version you have:

wp core version

This will give you the number of major and minor versions, currently installed for your website, e.g. 6.5.4.

Then we want to check if there is any update available:

wp core check-update --minor

Omitting the --minor flag will tell WP-CLI to look for major releases. If your WordPress site is up-to-date with minor releases, you’ll see the following message:

Success: WordPress is at the latest minor release.

If, however, you see a table with version, update_type, and package_url, then there is an update available, and you should update your website sooner rather than later:

Minor release updates should never break anything on your website—these are only bug fixes. Regardless, you should check your website to make sure everything works as expected. If something is broken, there are two things you should do:

  1. Revert update with: wp core update --version=6.5.4 --force (this is why you checked your version first). You have to use the --force flag every time you’re updating to the version that is not the latest, regardless of whether it’s a minor or major release.
  2. Check with your developer for the possibility to replace the plugin or theme which caused the break. If they don’t work with minor releases, then the code inside is most likely not following best practices.

And that’s it. You ran your first WP-CLI commands and did something useful. Most importantly, your website is still alive. Congratulations!

Similar principles can be applied to updating themes and plugins. Both have a --dry-run flag to allow you to just check which plugins and themes have available updates, without actually updating them:

wp plugin update --all --dry-run

To check the minor version, the --minor flag is also available:

wp theme update --all --minor --dry-run


If there are no updates available, you’ll see a No theme/plugin updates available message. If there are, you’ll see a table with all the plugins or themes that have available updates, their current and available versions, and the status (active or inactive).

To update only some plugins or themes, you can specify them by using the name from the above table:

wp plugin update akismet --minor

If you, for whatever reason, don’t want to run these updates in production, you can always test them first on a staging website. All serious hosting providers will provide you with a staging version of your website for these kinds of tests.

Monitoring file changes

Often, when your website is hacked, it will have some kind of changes in files. It can be in the form of a new file anywhere in your file system, or a change in existing files. You can use WP-CLI to monitor these changes.

Checking changes to your files is done with the verify-checksums subcommand: 

wp core verify-checksums

This will check your WordPress core files against known WordPress versions. If you want to check a specific version, you can specify it:

wp core verify-checksums --version=6.5.2

If the check fails, you’ll see this message:

Error: WordPress installation doesn't verify against checksums.

Before this message, you’ll also see the list of files that didn’t verify against checksum.

This might mean that your WordPress install is corrupted, but it can also be that you mistyped the version. To always be certain you typed the correct version, you can use:

wp core verify-checksums --version=$(wp core version)

As you can see, inside the command substitution, $(), we are executing the command to get the version number. So you don’t have to even know which version you have because different CLI tools work perfectly together, and you can provide values by nesting commands. Just wrap them inside $().

If all is good, you’ll see the following message:

Success: WordPress installation verifies against checksums.

It is possible to get this success message while malicious changes still happen. For example, there might be a new file in your installation even though no core files are modified. If this new file is added in /wp-includes or /wp-admin folders you will see a warning about the file that shouldn’t exist there:

Warning: File should not exist: wp-includes/install-wp.php

To include the root folder of your WordPress into this check, add the --include-root flag. 

wp core verify-checksums --include-root --version=$(wp core version)

Now you are left with wp-content as the only unchecked directory. In this directory, you should only have an index.php file and folders (or single PHP files) named as plugins you see in your WordPress dashboard. To check them all, you can run the same verify-checksums subcommand. If you want to also check small changes (such as in the readme.txt file), add the --strict flag.

wp plugin verify-checksums --all --strict

For any plugin that’s not hosted at WordPress.org you will get following warning:

Warning: Couldn't fetch response from https://downloads.wordpress.org/plugin-checksums/<plugin>/<version>.json (HTTP code 404).
Warning: Could not retrieve the checksums for version <version> of plugin <plugin>, skipping.

Again, this doesn’t necessarily mean that you have malicious code in your WordPress install. It only means that this plugin is not hosted at WordPress.org. It could be a custom plugin your developer created specifically for your website, or it can be a premium version of a plugin that you bought at some marketplace. 

If you are not sure how to deal with all of these warnings you see, you should copy and paste the output from the terminal into another document and send it to your developer for further investigation. This will decrease debugging time drastically, or prove to be a false alarm. Both of which are more desired than their alternatives.

User management

Sporadically, when hackers breach the website, they will change the users’ access. They can completely remove one or more users (mainly administrators) or just limit the access. In this situation, it is highly likely they breached through the WordPress dashboard by hacking the password or creating a new user by executing files in your WordPress install. 

But you have SSH access. And that makes you the most powerful person on your server. So first you want to check all users with administrator access:

wp user list --role=administrator

You will get a table with user IDs, usernames, display names, emails, roles, and registration dates. The most recent registered users should be suspicious if the usernames and emails don’t ring any bells. 

Keep in mind that you CAN see this list and perform all the actions even if your user doesn’t exist anymore. You don’t need access to the WordPress dashboard to be able to make any changes via WP-CLI.

From this point, you can silently revoke access to a suspicious user with following command (you need their username, email address, or user ID):

wp user remove-role <user_name|user_email|ID>

When a user has no role, WordPress doesn’t know what to do with them, so they have no access whatsoever. Even subscribers have access to their profile in the dashboard, while roleless users have none. 

If you are not sure which user is malicious, you can remove roles of all administrators by combining other CLI tools:

for i in $(wp user list --role=administrator --field=ID); do wp user remove-role $i; done

You might recognize command substitution here – $(). We are using it to get user IDs for every administrator. Instead of ID, you can use user_name or user_email. As there might be more than one, we need to execute the above command to remove the role for each user. That’s why we are running our IDs through a for loop where i represents the value for the user. Now we want to run wp user remove-role for each of the users, where the individual value is represented with $i. When we loop through all, we are done.

After running it, you will see a message that informs you about the success or possible failure:

Success: Removed <user_login> (<ID>) from <URL>.

Now you can create your own new user if needed but, since we don’t know how suspicious users were created, you want to run checks for file changes

When you are not sure which, if any, flags are mandatory for the subcommand, and you don’t want to go back and forth to help or documentation, a great help is the --prompt global parameter. It will prompt you with all the available flags (mandatory ones are inside <> tags), so you only have to type what’s unique for your use-case. This will make your commanding much easier and more intuitive:

wp user create --prompt

After this, you want to delete the history of your commands because you don’t want that password to be saved there. To do this, run:

history -c && history -w

Or you can omit the password here and use the “Forgotten password” feature on a WordPress login screen to set one.

Backups

As with staging websites, any serious hosting provider has some kind of backup service. However, some hostings don’t provide you with a download option but rather offer only restoring possibility. 

The only things you really want to have a backup for are the wp-content folder and database

First, let’s create backup folder:

mkdir backup

For databases, you can specify the file name or go with the WP-CLI default structure: <database_name>-<date>-<random_hash>.sql. The random hash is used to make sure no one can guess the name of a database file on your server but in full honesty, you should never keep any database backups on your server so you can name it as you wish, we are going to delete it soon anyway:

wp db export backup/database.sql

Now we want to prepare the wp-content folder by compressing it to zip and moving to new backup directory:

zip -rv backup/wp-content.zip ./wp-content -i "wp-content/themes/*" "wp-content/plugins/*" "wp-content/uploads/*"

Here, I’m adding only the /themes, /plugins, and /uploads folders to the zip file because I want to avoid unnecessary folders, such as /upgrades and all the folders various plugins create for their needs.

Breakdown of the zipping command:

  1. zip – obviously, use the zip command
  2. -rv – go inside every folder (recurse-paths) and tell me what you are doing (verbose messages)
  3. backup/wp-content.zip – save the archive in this path (and name it wp-content.zip)
  4. ./wp-content – use /wp-content as a source for the archive
  5. -i – include only the following files
  6. "wp-content/themes/*" – everything you find inside /wp-content/themes/ directory (and repeat that for /wp-content/plugins and /wp-content/uploads

Now I can zip the whole backup folder to be able to download it easily:

zip -rv backup.zip ./backup/
Video showing the whole procedure of creating backups.

This is how your root folder looks now:

.
├── backup
├── wp-admin
├── wp-content
├── wp-includes
├── backup.zip
├── index.php
├── license.txt
├── readme.html
├── wp-activate.php
├── wp-blog-header.php
├── wp-comments-post.php
├── wp-config.php
├── wp-config-sample.php
├── wp-cron.php
├── wp-links-opml.php
├── wp-load.php
├── wp-login.php
├── wp-mail.php
├── wp-settings.php
├── wp-signup.php
├── wp-trackback.php
└── xmlrpc.php

There are two ways to download backup.zip file: 

  • With curl command from your local terminal,
  • by navigating to the URL where your backup.zip file is.

I’m going to keep this simple and assume you never opened the terminal on your local machine (but you do have one) so we’ll go with the second option. 

If you run these commands inside your WordPress root, then you can easily download the zip file by navigating to https://<your-website.com>/backup.zip. The download will start automatically and, when finished, you should delete both backups from your server, zip, and directory:

rm -rf backup*

Now you have all the necessary backups on your machine, and you did it in less than 5 minutes, with no traces on your website 🤫

While this way of creating and downloading your backups is possible, it is not recommended to expose those files in such a way, especially NOT as a regular backup solution. For regular backups consult your developer and hosting provider.

It is recommended to create the backup folder one level above the WordPress root and use scp command from your local terminal to download it.

Maintenance mode

There is a great explanation of how to use WP-CLI for maintenance mode in official documentation. I suggest you go over there and get additional information about it.

Want to explore more?

If you feel confident enough for more advanced checks and actions, I recommend looking into:

None of these commands come with WP-CLI out of the box, so you have to install them as separate packages.
A bonus here is a list of plugins that have their own custom commands, as well as some useful tips on how to combine WP-CLI with other CLI tools for more magic performances.

Conclusion

If you’ve read this far, I hope you came to the following conclusions:

  • WP-CLI is an extremely powerful tool for managing WordPress websites, and one you want to utilize when fast actions are needed, but also to prevent these necessities. 
  • You can use WP-CLI as long as you have SSH access to your website. Nothing ever needs to be broken, you always have help available.
  • What you’ve seen in this article is just the tip of the iceberg. So many things are possible, and you can explore them all inside the nearest terminal.

Props to @bph and @greenshady for peer review, and @westonruter for recommendation.

14 responses to “Website security checks – WP-CLI for site owners and administrators”

  1. Weston Ruter Avatar

    I don’t think the backup.zip file and backup folder should be located in the document root, even temporarily, since this could undesirably expose the files to the public. I suggest that such private data be put in one directory above the docroot or in the user home directory instead. This will ensure the backup data remains private.

    1. Milana Cap Avatar

      I’ve been thinking about this and having second thoughts between making it as simple (and beginner friendly) as possible, while keeping best practices in mind. And that part IS a security hole, you are right. I added the warning note about this.

      Thank you for pointing this out.

  2. Weston Ruter Avatar

    Since you’re already connected to the site via SSH, then you can use scp to copy the file from the remote server to your local machine without having to expose it publicly to download with curl.

    1. Milana Cap Avatar

      The idea for the article is to be beginner friendly as much as possible and don’t overwhelm with all the different CLI tools. But it is a good suggestion so I added the note about this. Thank you.

  3. Vin Avatar
    Vin

    It seems that you skipped the first few steps. You state at the beginning of your article that wp-cli is already installed but it wasn’t pre-installed on my Mac, I had to install it. I followed the instructions from here: https://wp-cli.org. But even after installing I found that the test command to check the Phar file to verify that it’s working: “php wp-cli.phar –info” didn’t work as “php” is not recognised as a command. Neither is “wp”. Unfortunately, neither your article nor the instruction on the wp-cli website mention what to do if the php command is not found and so I am stuck before I’ve even begun. Can you please advise on where to go from here? Thanks.

    1. Milana Cap Avatar

      Hello,
      This article wants to avoid exactly that problem. Systems are different and to run WP-CLI on your local machine you need to have its requirements met. WP-CLI is built with PHP, therefore, you need to have PHP installed to run it.

      As I said, this article wants to avoid the problem of different systems and requirements potentially not being met, so we are running these commands on hosting servers of those providers that offer already installed WP-CLI.

  4. Anh Tran Avatar

    There are a lot of great features in WP-CLI. One thing that I love is you can backup the DB right from your local computer, with –ssh flag. So you don’t have to store the DB file on the server (which can be a security issue) and download it. And then you can easily import it to the local site, to replicate the production site. Just 2 commands, very quick and easy.

  5. Simon Avatar

    Nice article, but you don’t really explain why you would do this, apart from if your site has been hacked. I know it’s not user friendly, but presumably you could create a bash script for all this and run it on multiple sites, even when you aren’t there?

    1. Milana Cap Avatar

      Of course, you can and should make it as automated as possible. By all means, combine it with other CLI tools. That’s where the real CLI powers lie.

      This is, however, an article for beginners—site owners and administrators who have never dared use WP-CLI. Hopefully, this article will help them see its potential and encourage them to try it out.

      1. Paul Avatar
        Paul

        Is there an article for advanced users? At least provide a link to that. If none, how about collaborating with me to write one? This is open to other contributors as well. It can be like a part two for this article. I want to know all the cool stuff you can do with wpcli without the beginner friendliness.

        1. Milana Cap Avatar

          There are many more advanced resources available on various places – personal blogs, hosting provider’s blogs, YouTube videos etc. WP-CLI allows you to use it in a very complex to very simple ways, all of which can be both advanced and less advanced. So it is very difficult to pick one, or even a few, and post it as a next advanced step.

          That being said, I’d be very happy to collaborate in any capacity on another article. Please propose your idea in Core Dev Blog repo: https://github.com/WordPress/developer-blog-content/discussions/new?category=topic-ideas

          It will be discussed about at the next editorial meeting. If accepted, you can take over and I’ll be there to help. Here’s a guideline to help you get started: https://developer.wordpress.org/news/how-to-contribute/

  6. micle Avatar
    micle

    Great article, but you don’t fully explain why this approach would be necessary, aside from in cases of a site being hacked. While it’s not the most user-friendly method, couldn’t you create a bash script to automate this process across multiple sites, even in your absence?

  7. DaoNQ Avatar

    Useful! Thank you.

  8. NICHOLAS AMOL GOMES Avatar
    NICHOLAS AMOL GOMES

    Great job as given Thanks for sharing the post

Leave a Reply

Your email address will not be published. Required fields are marked *