Chapter 8. Secure WordPress

Hackers beware! This chapter is packed full of tips and advice on how to make WordPress sites more secure and hopefully prevent them from falling prey to any malicious intent.

Why It’s Important

No matter what size website you are running, security is something that you do not want to overlook. Any size site can fall victim to hackers or malware. Being knowledgeable and proactive about WordPress security will help you be less vulnerable and hopefully avoid any attacks.

One of the most popular types of attacks is called a brute-force attack, in which a bot or script of code tries to gain access to your site by guessing the correct username and password combination. It may not sound that dangerous, but keep in mind that these bots are huge networks of computers making hundreds or even thousands of guesses every second! Even if these bots don’t gain access to your WordPress administrator, they will often take your site down anyway through the sheer amount of resources it takes your server to respond to the malicious requests. This is called a denial of service (DoS) attack, and can be caused by a targeted attack or by automated spammers and brute-force hacks.

In this chapter, we discuss the standard WordPress installation’s built-in security features, in addition to other tips that you can easily follow to make your site more secure. We’ll also highlight some plugins that can help with other issues, such as spam.

Some very bad things that can happen to you if you decide to not read the rest of this chapter. Here are some pretty frequent scenarios:

  • You attempt to pull up your website but find that it’s not there anymore. Downtime is bad! Hopefully you have a backup and can restore it quickly.

  • You notice that you start showing up in search results for Viagra and other male enhancement drugs. This can be bad for business if your website is not specifically selling these drugs.

  • Your application is sending out emails to all of your members with links to download a computer virus. Nobody wants that.

  • Your application is hacked and the personal information of your members (their names, addresses, phone numbers, and email addresses) is exposed.

  • Your website is hacked and is used to infect other websites with malware. This is the quickest way to get delisted from Google search results and other important directories.

Security Basics

These are the simplest but most important security tips to consider. Pay attention here because it could save you a lot of time, money, and upset visitors/members.

Update Frequently

The first and most important security tip is to always make sure you upgrade to the most recent version of WordPress as soon as a new version becomes available and also always update any plugins/themes that you have installed on your site. Many of the updates that are pushed out involve security updates; therefore, it is always important to upgrade your software in order to stay up to date and safe.

Don’t Use the Username “admin”

Another important item to take care of is making sure not to use “admin” as one of your user accounts. Many bots will automatically try to log in to your site with this username. Knowing that most people don’t change this account is half the battle; all they really need to focus on is guessing the password. When installing WordPress, the default username will be “admin” unless you specifically change it, and you should specifically change it! If you are already using WordPress under the username “admin,” you should create a new user account with an administrator role, log in with that new user, and delete the default administrator account. Make sure you change over any posts or pages created by your administrator account to this new account.

Use a Strong Password

Choosing a secure password is also very important, especially for your administrator accounts. Don’t use one word or one name. Jumble your password up and make it not connected to you personally.

Make sure your password is a combination of upper- and lowercase letters as well as numbers and special characters. A good password should also be at least 10 characters long; the more characters you use, the stronger your password will be. If you are having trouble coming up with one yourself, just mash on your keyboard a bit or use a service like Random.org. Make sure you memorize it or copy it somewhere and secure it properly. WordPress will tell you whether you are using a strong password; please take this into consideration.

Examples of Bad Passwords

Following are some examples of bad passwords:

  • password

  • password123

  • pa55w0rd

  • 123456789

  • qwerty

  • batman

  • mustang

  • letmein

Using any variation of password or single words, numbers, or names is a bad idea:

  • usmarine (Brian was in the Marines)

  • brianmessenlehner (Brian’s first and last name)

  • jason&kim050507 (Jason’s name, his wife’s name, and their anniversary)

  • Dalya-Brian (kids’ names)

  • ThaiShortiMaxx (pets’ names)

  • IAMAWESOME! (everybody knows this, so it could be easy to guess)

Anybody who knows anything about us or our families could potentially guess passwords like these.

Examples of Good Passwords

Following are some examples of strong passwords:

  • U$s(#8H27@!

  • !lik32EaTF1$h&CHIp5

  • #Uk@nN0tBr3akTh1s$h1t!!!

  • [0mG-LoL-R0Fl-T0T3$CraY]!

It can be a pain in the neck and take an extra second or two entering a good password, but it’s well worth it if it can prevent your website/application from being hacked.

Hardening WordPress

The work of making a website more secure is often referred to as hardening. The WordPress Support section “Hardening WordPress” has similar information to what’s in this chapter, plus other things we don’t cover. Read this chapter first, then see “Hardening WordPress” for more information.

Let’s go over a few techniques for making it harder for your application to be hacked.

Don’t Allow Admins to Edit Plugins or Themes

By default, WordPress allows administrators to edit the source code of any plugin or theme directly in the web browser. You should disable this functionality so that if a hacker is able to log in to one of your administrator accounts, they can’t add malicious code via the administrator user interface for editing code. To disable this functionality, add this code to your wp-config.php file:

define( 'DISALLOW_FILE_EDIT', true );

Change Default Database Tables Prefix

The standard WordPress installation uses wp_ as a prefix for all tables in the database. By simply changing this prefix to something else, you will make your site far less vulnerable to hackers who attempt SQL injections and assume you’re using the generic wp_ prefix. On a brand-new WordPress installation, you have the option to use any table prefix you want; you should change the default wp_ prefix to something custom.

To do this on a WordPress site that is already up and running, follow these steps:

  1. Make a database backup just in case you mess this up!

  2. Open wp-config.php and change $table_prefix = wp_; to $table_prefix = anyprefix_;.

  3. Update the existing table names in your database to include your new prefix with the following SQL commands using phpMyAdmin or any SQL client such as MySQL Workbench:

    rename table wp_commentmeta to anyprefix_commentmeta;
    rename table wp_comments to anyprefix_comments;
    rename table wp_links to anyprefix_links;
    rename table wp_options to anyprefix_options;
    rename table wp_postmeta to anyprefix_postmeta;
    rename table wp_posts to anyprefix_posts;
    rename table wp_terms to anyprefix_terms;
    rename table wp_term_relationships to anyprefix_term_relationships;
    rename table wp_term_taxonomy to anyprefix_term_taxonomy;
    rename table wp_usermeta to anyprefix_usermeta;
    rename table wp_users to anyprefix_users;
Note

You will need to run a similar rename SQL query for any custom tables added by your app or plugins you are using.

Using SQL commands or a SQL client, update any of the instances of wp_ in the prefix_options and anyprefix_usermeta tables and change any values like wp_ to prefix_:

update anyprefix_options set option_name = replace(
option_name,'wp_','anyprefix_');
update anyprefix_usermeta set meta_key = replace(
meta_key,'wp_','anyprefix_');

Test out your site and make sure everything is working as it should.

If you don’t feel comfortable making these changes manually, there are plugins available that can change your table prefix for you:

Move wp-config.php

The WordPress wp-config.php file stores valuable information like your database location, username, and password and your WordPress authentication keys. Since these values are stored in PHP variables and they are not displayed to the browser, it is not likely that anybody could gain access to this data, but it could happen. You can move wp-config.php to one level above your WordPress install, which in most cases should be a nonpublic directory. If it doesn’t find it in your root directory, WordPress automatically looks one level up for wp-config.php. For example, move /username/public_html/wp-config.php to /username/wp-config.php.

You can also store wp-config.php as any filename in any directory location. To do this, make a copy of wp-config.php, name the copy whatever you want, and move it to any directory above your root install of WordPress. In your original wp-config.php file, remove all of the code and add an include to the relative path and filename of the copy you made. For example, copy /username/public_html/wp-config.php to /username/someotherfolder/stuff.php. Change the code in the wp-config.php file to include('/username/someotherfolder/stuff.php');.

Hide Login Error Messages

Normally, when you’re trying to log in to your site, WordPress displays an error message if you’ve entered the wrong username or password. Unfortunately this lets hackers know exactly what they are doing wrong or right when attempting to access your site.

Luckily there is a simple fix for this: add a line of code into your theme functions.php file or in a custom plugin that will hide or alter those messages:

add_filter( 'login_errors', function ( $message ) {
    return "Invalid username or password.";
} );
Note

The preceding code uses an anonymous function as the callback in the second parameter of the add_filter() call. This requires PHP version 5.3 or higher. You could also just define a named function above the add_filter() call, but then this wouldn’t be “one line” of code.

Hide Your WordPress Version

Many bots scour the internet in search of WordPress sites to target specifically by the WordPress version they are running. These bots look for sites with known vulnerabilities they can exploit. By default, WordPress displays the following code within the <head></head> of every page:

`<meta name="generator" content="WordPress 3.8.1" />`

You can easily hide the version of WordPress you are using by implementing the following code:

add_filter( 'the_generator', '__return_null' );
Note

There are various ways to detect whether a site is using WordPress and what version. For example, if no script version is specified, the WordPress version is appended to the URLs of JavaScript files. There are other, even more subtle ways in which a determined hacker could detect the version of WordPress you are using. Still, every little bit helps to thwart largely automated attacks that are constantly floating around the internet.

Don’t Allow Logins via wp-login.php

Some bots are smarter than others. We just discussed hiding your WordPress version from some bots, but sometimes all a bot needs to know is that you’re using WordPress. This is easy if it sends a POST request to wp-login.php. Once a bot knows wp-login.php exists, it can start trying to log in to your site.

We like to redirect wp-login.php to the home page, which prevents bots from specifically trying to log in using this file. Follow these steps to make an alternative login page and hide the default wp-login.php login page:

  1. Add the following rewrite rule to your .htaccess file:

    RewriteRule ^new-login$ wp-login.php

    Note that /new-login/ will be the URL you can use to actually log in to wp-admin. You can change this to whatever you want.

  2. In your theme functions.php file or in a custom plugin, add this code:

    function schoolpress_wp_login_filter( $url, $path, $orig_scheme ) {
        $old = array( "/(wp-login.php)/" );
        $new = array( "new-login" );
    	return preg_replace( $old, $new, $url, 1 );
    }
    add_filter( 'site_url', 'schoolpress_wp_login_filter', 10, 3 );
    
    function schoolpress_wp_login_redirect() {
     if ( strpos( $_SERVER["REQUEST_URI"], 'new-login' ) === false ) {
    		wp_redirect( site_url() );
    		exit();
     }
    }
    add_action( 'login_init', 'schoolpress_wp_login_redirect' );

If you don’t want to write any custom code, you can use the following plugins to achieve similar results:

Add Custom .htaccess Rules for Locking Down wp-admin

If you are the only user that needs to log in to the backend of your application, or if you have only a handful of backend users, you can restrict access to the backend by certain IP addresses. Create a new .htaccess file in the wp-admin directory of your WordPress installation and add the following code, replacing 127.0.0.1 with your actual external IP address. Go to http://ipchicken.com/ if you are not sure of your external IP address:

order deny,allow
allow from 127.0.0.1 #(repeat this line for multiple IP addresses)
deny from all

If you suspect that certain IP addresses hitting your application are bots or malicious users, you can block them by their IP addresses by using the following code:

order allow,deny
deny from 127.0.0.1 #(repeat this line for multiple IP addresses)
allow from all

If people really want to get around their banned IP address, they will use a proxy server.

If you think your IP address or that of your backend users may change often or you have far too many backend users to manage all of their IP addresses, you can add a separate username and password to access the wp-admin directory. This adds a nice second layer of authentication because all of your backend users will be required to enter an htaccess username and password and their standard WordPress username and password:

AuthType Basic
AuthName "restricted area"
AuthUserFile /path/to/protected/dir/.htpasswd
require valid-user

Notice the AuthUserFile line; you will need to create a .htpasswd file somewhere in a directory above or outside of your WordPress install. In this file, you will need to add a username and password. The password can’t just be plain text; use a tool like htaccess password generator to create an encrypted password.

So the username/password for letmein/Pr3tTyPL3a$3! after encryption should be letmein:E5Dj7cUaQVcN.

Add the entire encrypted string letmein:E5Dj7cUaQVcN. to your .htpasswd file; and when users try to go to /wp-admin, they will be prompted for a username and password. Make sure to let your backend users know what this username and password is and tell them not to share it with anybody.

SSL Certificates and HTTPS

When accepting sensitive information through a web form—for example, a credit card number—you should encrypt that information by loading and submitting the form over SSL or HTTPS. First, some definitions:

SSL

Stands for “Secure Sockets Layer” and is the technology that encrypts data that is transferred to and from a web page.

HTTP

Stands for “Hypertext Transfer Protocol.” This is the standard protocol for serving web pages without encryption.

HTTPS

Stands for “HTTP Secure.” This is the protocol for serving web pages with SSL encryption.

There are many options when it comes to configuring SSL on your server and installing an SSL certificate. The bottom line is that all sites (not just ecommerce sites) should be set up to serve all traffic (not just sensitive traffic) over HTTPS these days. We’ll cover a few options for doing this in the following sections.

Installing an SSL Certificate on Your Server

First, make sure that you have SSL enabled on your web server. How to do that will depend on your specific host and web server. DigitalOcean has great system administrator documents in general and a particularly good article on setting up SSL with Apache.

After enabling the SSL service on your host, you’ll need an SSL certificate to use with it. You can use self-signed certificates for testing purposes, but modern-day browsers will show some fairly dire warnings when you browse to a site using a self-signed certificate. Figure 8-1 shows the warning displayed to Chrome users.

Chrome SSL warning
Figure 8-1. Chrome SSL warning

For production environments, you’ll want to use a public-key certificate from a certificate authority, or CA. You can purchase public-key certificates, though they’re usually bundled or offered as an add-on to your web-hosting package. You can also use public-key certificates (SSL certificates) purchased from third parties. A good CA certificate will be trusted by all modern web browsers, which is what gives you the green or golden padlock icon on your website instead of a broken or red padlock.

There are good options now for paid or free CA certificates. What you’re really doing when you use a CA certificate is confirming that you actually own the domain on which you are using the certificate. Ownership of the domain is usually confirmed via email to an address on the domain. Or, in the case of Let’s Encrypt, the confirmation is done through automated scripts run from the server in question.

Again, every website should install an SSL certificate. In the first edition of this book, we covered methods to serve the admin and checkout pages over HTTPS while the rest of the site was served over unsecured HTTP. We no longer recommend this kind of hybrid setup. The internet has moved to a place where it is assumed that all websites are served entirely over HTTPS. This is part of a larger “security by default” movement.

There are a number of reasons to set up HTTPS on your entire site:

  • Security by default. You might imagine that only your login and checkout pages really need to be secure, but what happens when your site is updated to show a login form in the sidebar? Now every page on your site needs to be secure. If your entire site is served over HTTPS, you won’t accidentally introduce an unencrypted form anywhere on the site.

  • Internet consumers are trained to look for that padlock (see Figure 8-2). Both savvy and nonsavvy users will feel better seeing it. Further, modern browsers will show some pretty scary warnings if parts of your site are not served over HTTPS.

  • Google and other search engines have started boosting sites that are served entirely over HTTPS in their search rankings.

  • There is no longer a CPU hit to your server when you use HTTPS. The web server stack has been updated to better handle HTTPS and frankly expect it, so you can no longer use page load times as an excuse to disable HTTPS on your site.

Various padlocks across browsers
Figure 8-2. Various padlocks across browsers

Using one directory for HTTPS and HTTP traffic

Besides using a CA certificate, the other thing to do when setting up SSL is to have your HTTPS directory point to your HTTP directory through a symbolic link, or symlink for short. A symlink is like a shortcut in a Windows PC. The symlink points to another directory rather than being a directory of its own.

The end result of using a symlink for your HTTPS directory is that the same .php source files will be loaded when people visit https://yoursite.com as when they visit http://yoursite.com. Your server will make sure that the traffic through the HTTPS link is encrypted and both WordPress and any ecommerce plugin you may be using will make sure that the correct secure page is shown to the user when being served over SSL.

Assuming your HTTP directory is called html and you want your HTTPS directory to be called ssl_html, you would issue the following Linux command to create a symlink to that directory: ln -s http ssl_http.

WordPress Login and WordPress Administrator over SSL

Serving your checkout page over SSL is the minimum you can do to secure the private data passed to and from your site. You can also set up WordPress to use SSL on the login page, in the administrator dashboard, across the entire site, or only on select pages.

For SSL logins in WordPress, you set the FORCE_SSL_LOGIN constant to true in your wp-config.php file. Place the following line of code above the “That’s all, stop editing! Happy blogging.” comment at the end of the file:

define('FORCE_SSL_LOGIN', true);

To use SSL on the login page and in the administrator dashboard, use the following FORCE_SSL_ADMIN constant instead:

define('FORCE_SSL_ADMIN', true);
Note

The FORCE_SSL_ADMIN constant supersedes the FORCE_SSL_LOGIN constant. You should set only one or the other constant to true. If FORCE_SSL_LOGIN is false and FORCE_SSL_ADMIN is true, your login page will still be served over SSL.

Debugging HTTPS Issues

Now we’re going to write a quick little function to filter URLs generated by WordPress to use the same protocol as the current page. Remember earlier that we talked about how URLs like https://yoursite.com/some-page (HTTP) that show up on a page like https://yoursite.com/checkout (HTTPS) will cause your browser to show a security warning:

function my_https_filter($s) {
	if(is_ssl())
		return str_replace("http:", "https:", $s);
	else
		return str_replace("https:", "http:", $s);
}
add_filter('bloginfo_url', 'my_https_filter');
add_filter('wp_list_pages', 'my_https_filter');
add_filter('option_home', 'my_https_filter');
add_filter('option_siteurl', 'my_https_filter');
add_filter('logout_url', 'my_https_filter');
add_filter('login_url', 'my_https_filter');
add_filter('home_url', 'my_https_filter');

The is_ssl() function provided by WordPress will return true if the current page was loaded over HTTPS. The is_ssl() function checks specifically if the $_SERVER['HTTPS'] global is set to on or 1 or if the $_SERVER['SERVER_PORT'] global is set to 443. Some server setups behind load balancers or reverse proxies will load HTTPS pages without setting those globals properly in PHP. You can fix this by adding the following code to your wp-config.php file.

if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
		&& $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
	$_SERVER['HTTPS'] = 'on';
}

Our https filter uses the str_replace() function to swap “http:” for “https:”, or vice versa. We set this filter to run on a number of built-in WordPress hooks used at various places throughout the WordPress codebase where URLs are generated.

When you output URLs in other places of your custom application code, be sure to use the home_url() function to make sure the URL is generated correctly and the my_https_filter is run on it.

Avoiding SSL Errors with the “Nuclear Option”

The my_https_filter() function ensures links that show up on a page use the correct protocol. However, sometimes raw http://… URLs may be hardcoded into your posts, or maybe a plugin you use doesn’t use the built-in WordPress functions like it should when outputting same-site URLs or loading JavaScript or CSS files. Figure 8-3 shows the Chrome Developer Tools Console, which can help locate errors.

An SSL error in the Chrome Developer Tools Console. Use the Chrome Developer Tools Console to find SSL errors, or use the Nuclear Option to avoid them.
Figure 8-3. An SSL error in the Chrome Developer Tools Console

In these cases, you can try to find each case of a bad URL and fix the link in your posts or code to use a relative URL or the proper WordPress function to make sure it will output on the frontend using the proper protocol. However, it’s sometimes easier to use what we call the Nuclear Option:

constant('MY_SITE_DOMAIN', 'yoursite.com');

function my_NuclearHTTPS() {
	ob_start("my_replaceURLsInBuffer");
}
add_action("init", "my_NuclearHTTPS");

function my_replaceURLsInBuffer($buffer) {
  global $besecure;

	//only swap URLs if this page is secure
	if(is_ssl())
	{
/*
okay swap out all links like these:
* http://yoursite.com
* http://anysubdomain.yoursite.com
* http://any.number.of.sub.domains.yoursite.com
*/
$buffer = preg_replace(
      '/http://([a-zA-Z0-9.-]*'.str_replace('.','.',MY_SITE_DOMAIN).')/i',
      'https://$1',
$buffer
      );
	  }

	return $buffer;
}

First, we need to make sure we define a constant MY_SITE_DOMAIN and set it to the second-level domain (SLD) for your site. Your site_url() set in WordPress may be http:/<www.yoursite.com, but we are interested here in just the yoursite.com part of that.

Then, my_NuclearHTTPS() fires on the init hook and uses the PHP function ob_start() to turn on output buffering. Output buffering means that all output generated by PHP (e.g., via echo function calls or inline HTML) goes into a buffer string instead of straight to the browser. Then, when PHP is finished generating all output (or if you call the ob_end_flush() function first), the buffer string is passed to a callback function, which is my_replaceURLsInBuffer() in this case.

The my_replaceURLsInBuffer() function filters the buffer string, swapping out “http:” for “https:” on every link. The regular expression magic we’re doing in the preg_replace() call there makes sure that links to any subdomain using the same domain (why we needed to set the MY_SITE_DOMAIN constant) will also be filtered.

So you might have caught on by now why we call this the “Nuclear Option.” Instead of finding the source of bad URLs in your app and fixing them, we just fix all of the URLs at once before sending the output to the browser. There will be a small performance hit here, depending on how large your HTML output is. But this method can be useful in a pinch, especially if you are using many third-party plugins that you can’t or don’t want to fix to output site URLs properly.

Note

The Really Simple SSL plugin includes many of the HTTPS fixes we’ve mentioned, as well as other tools to help you get HTTPS working properly on your WordPress site.

Back Up Everything!

It is important to make regularly scheduled backups of your site’s content (your database) as well as the wp-content folder. This makes it much easier to restore your site in the event that it does fall victim to a hacker. We recommend scheduling a backup at the very least once a week, but depending upon how much new content you are adding, you may feel that you need to increase or decrease the frequency. Of course, a daily backup is always the best choice.

Note

Do You Know Where Your Backups Are?

Do you know if you can really use your backups to recover your site? Every few months, try to rebuild your site from your backups. This ensures that they’re really working, that you’re backing up everything you need to, and that you can quickly restore your site from backups. There are plenty of horror stories told by people who thought they had a backup plan only to find that the backups were corrupt, incomplete, or otherwise not useful for restoring the site.

Scan, Scan, Scan!

Scanning or monitoring your application is essential to know whether you have been attacked. If your application is ever hacked, it is important to know immediately, so you can quickly address the issue.

Be proactive about protecting your web application against malware. There are several services that will scan your web applications for you so you can take a more hands-off approach. We recommend using Sucuri. Not only will Sucuri find malware and alert you if your application has been infected, but it will also clean it up for you. Tony Perez, the COO of Sucuri, is also a former US Marine and a martial arts master, so why wouldn’t you want Sucuri to have your back? Sucuri also has a great security plugin for WordPress.

Useful Security Plugins

In the following sections are some more useful and powerful WordPress plugins to help you increase security for your application and also help you to recover quickly if you fall victim to a malicious attack.

Spam-Blocking Plugins

Spam is a problem for every website on the internet. These plugins will help.

Akismet

This plugin is used to block comment spam from getting through to your site. It was developed by Automattic, also the creators of WordPress, and therefore comes standard with any new WordPress install. Although the plugin will be installed on your site, you will need to activate it by registering for an API key at https://akismet.com/. An API key is free if your site is for personal use; however, there is a small charge for business websites. The way Akismet works is each time a comment is posted to your site, Akismet will run it through a series of tests to ensure it is a real comment; if it is identified as spam, it is automatically moved to the spam folder in your dashboard. This saves you tons of time having to sort through all of your comments and determine which ones are spam and which are legitimate comments.

Bad Behavior

The Bad Behavior plugin works to block link spam from your site and functions best when run in conjunction with another spam service. It looks not only at the content of the spam, but also at the method through which the spam is being delivered by the spammer and the software being used, and blocks that as well.

Backup Plugins

Backups are very helpful to have in the event that your site is compromised. Here are a few popular backup plugins.

BackupBuddy

As we mentioned in Chapter 3, BackupBuddy is a premium plugin that lets you make backups of all your WordPress site content for safekeeping, restoring, or moving your site. Backups can be scheduled and the files emailed to you or sent to a storage site such as Dropbox or an FTP server. This plugin’s restore option easily restores themes, widgets, and plugins. With BackupBuddy you can use the WordPress dashboard to move your site to a new server or domain, a handy feature if you work on a Dev server and move the sites to a Production environment upon launch.

VaultPress

VaultPress is another plugin created by the team at Automattic and offers users the opportunity to have all of their site content backed up in real time on cloud servers. Once installed, this plugin will automatically detect any changes to the content on your site as well as site settings and then update the backup copy with those changes. The plugin also features a one-click database restore in the event that your site ever becomes compromised. This is a premium plugin, meaning there is a fee for service, and different levels are offered. The premium version of the plugin also includes a daily security scan of your site to detect any issues as well as fixes for those issues.

Firewall/Scanner Plugins

The plugins that follow are useful for detecting and mitigating the kinds of automated attacks that plague every website on the internet.

WordFence

WordFence works as a type of firewall for your site by scanning incoming traffic and then blocking all kinds of different malicious requests. You can also perform an on-demand scan of your site and detect any areas of vulnerability in your site’s security. Upgrade to the paid version to use some of the premium tools and use a more up-to-date database of malware and vulnerabilities.

All In One WP Security & Firewall

The All In One WP Security & Firewall plugin has a firewall and scanner similar to WordFence’s. It also has tools to harden your login and user security. One important security issue this plugin helps with is changing your database table prefix, which can be tricky if you are not that familiar with the standard database structure.

Exploit Scanner

Maintained by Automattic, the Exploit Scanner plugin will scan through all the files on your site and then alert you if it finds anything that looks like it could be a potential threat.

Login and Password-Protection Plugins

The plugins here deal specifically with limiting access to the WordPress login page and dashboard in general.

Limit Login Attempts

The Limit Login Attempts plugin is great for fighting off brute-force attacks, such as someone running an automated script that will try to log in to WordPress using random combinations of words. By default, WordPress will allow an unlimited amount of login attempts, and this plugin limits the number of those attempts. If someone tries x times to log in and fails each time, they will be blocked from attempting to log in again for a set amount of time.

AskApache Password Protect

AskApache Password Protect is different from other WordPress security plugins in that it works at the network level to prevent attacks, rather than at the site level. You choose a unique username and password that then protect your login page and entire wp-admin folder. This plugin does require the use of an Apache web server and web host support for .htaccess files.

Writing Secure Code

You want to make sure any custom code you write is secure and not hackable. If you take notice and apply the following methods, you should be in pretty good shape against attacks.

Check User Capabilities

Each of your users has unique standard or custom roles and capabilities. If you are writing some code that provides custom functionality for your application’s administrators, make sure to give access to administrators, and only administrators. There are a few built-in WordPress functions for telling you whether a user has certain roles or capabilities. All of these functions are located in wp-includes/capabilities.php and return a Boolean of whether the user has the passed-in role name or capability. You can pass in any default or custom-made roles or capabilities.

user_can( $user, $capability )

Whether a particular user has a particular role or capability.

$user

A required integer of a user ID or an object of the user.

$capability

A required string of the capability or role name.

current_user_can( $capability )

Whether the current user has a particular role or capability.

$capability

A required string of the capability or role name.

current_user_can_for_blog( $blog_id, $capability )

Whether the current user has a particular role or capability for a particular site on a multisite network.

$user

A required integer of a blog ID.

$capability

A required string of the capability or role name.

In the following code, we don’t want to let ordinary users into the backend of our application. We want them to interact only with the custom UI we created within the theme on the frontend, so we will redirect anybody who is not an administrator and may wander to /wp-admin back to the frontend:

function schoolpress_admin_check() {
	global $user_ID;
	if ( ! user_can( $user_ID, 'administrator' ) ) {
        wp_redirect( site_url() );
    }
}
add_action( 'admin_init', 'schoolpress_admin_check' );
Note

Another common practice in many WordPress plugins is to test for the manage_options capability rather than the administrator role. On a default WordPress install, only the administrator has this capability anyway, but checking for manage_options instead of administrator will ensure that your check works on sites with custom roles.

For a complete reference of standard default WordPress roles and capabilities, see Chapter 6 or the WordPress Codex page.

Custom SQL Statements

Sometimes the built-in WordPress functions that interact with the database may not be enough for your needs, and depending on what you are building, you may want to write custom SQL statements. When writing your own SQL statements, you need to make sure you do so in a way that will not allow for any potential SQL injections. First, always use the $wpdb object and make sure to escape and prepare all custom SQL statements.

As we talked about in Chapter 3, the $wpdb object can be used to access any standard or custom tables in your WordPress database and provides easy-to-use methods for doing so. One very important thing to remember is that when writing custom queries with any dynamic values being passed in, you need to use the esc_sql() function or the prepare() method to sanitize and escape those dynamic values. By sanitizing and escaping dynamic values, you are making sure those values are not made up of invalid characters or malicious SQL code that can hijack your query (SQL injections).

The esc_sql() and $wpdb->prepare() functions are covered in detail in Chapter 3.

Data Validation, Sanitization, and Escaping

Do not trust your users! Again: do not trust your users! Don’t be that web application, website, or blog that spreads malware.

Validate, sanitize, and escape every piece of data going into and coming out of your database. You want to make sure that the data your users are submitting to your database is in the format it should be in; the database doesn’t care what the data is as long as the data being submitted is of the same datatype.

For example, suppose that you have a custom form used to collect user data with a textbox for date of birth (DOB). You plan on storing the DOB as user meta in the meta_value column of the wp_usermeta table. The meta_value column has a datatype of longtext, meaning the value can be super-duper long1 and the database isn’t going to care what value you store there. It’s up to you as the developer to make sure the data being stored as DOB is a date and nothing else.

So what exactly is the difference between validation, sanitization, and escaping?

Validating

The process of making sure the data received from the end user is in the correct format you expect it to be in. You want to validate data before saving it into the database.

Sanitizing

The process of cleaning data received from the end user before saving it to the database or using it in your app.

Escaping

The process of cleaning data you may already have before displaying it to the end user, saving it to the database, or passing it off to an API.

Now you know!

You want to validate and sanitize any data submitted to your app through form submissions, URL parameters, or API calls. You want to escape any data before putting it into your database or echoing it out to the screen. When pulling data out of your database, you want to sanitize it just to be safe in case somehow you are storing unsanitized data.

PHP has validation and sanitization functions, but WordPress has its own helper functions built in. This is a book about WordPress, so let’s talk about some of those functions.

Note

Most sanitization and escaping helper functions are located in wp-includes/formatting.

esc_url( $url, $protocols = null, $_context = ‘display’ )

Checks and cleans a URL by checking whether it has the proper protocol, stripping invalid characters, and encoding special characters. Use this if displaying a URL to an end user:

$url

A required string of the URL that needs to be cleaned.

$protocols

An optional array of whitelisted protocols. Defaults to array( http, https, ftp, ftps, mailto, news, irc, gopher, nntp, feed, telnet, mms, rtsp, svn ) if not specifically set.

$context

An optional string of how the URL is being used. Defaults to display, which sends the URL through wp_kses_normalize_entities() and replaces &amp with &#038; and ' with &#039;.

esc_url_raw( $url, $protocols = null )

This function calls the esc_url() function but passes db as the value for the $_context parameter. Do not use this function for displaying URLs to the end user; only use it in database queries.

esc_html( $text )

Escape HTML blocks in any content. This function is a nice little wrapper for the _wp_specialchars() function, which basically converts a number of special characters into their HTML entities:

$text

A required string of the text you want to escape HTML tags on.

esc_js( $text )

Escapes strings in inline JavaScript. Escaped strings need to be wrapped in single quotes for this to work:

$text

A required string of the text you want to escape single quotes, HTML special characters ( " < > & ), and fix line endings on.

esc_attr( $text )

Escapes HTML attributes and encodes such characters as <, >, &, ”, and ‘. This is important to use when including values in form input elements such as ID, name, alt, title, and value:

$text

A required string of the text you want to escape HTML attributes on.

esc_textarea( $text )

Escaping for textarea values. Encodes text for use inside a <textarea> element:

$text

A required string of the text you want to escape HTML on.

sanitize_option( $option, $value )

This function can be used to sanitize the value of any predefined WordPress option. Depending on what option is being used, the value will be sanitized via various functions:

$option

A required string of the name of the option.

$value

A required string of the unsanitized option value you wish to sanitize.

sanitize_text_field( $str )

Sanitizes any string input by a user or pulled from the database. Checks for invalid UTF-8; converts single < characters to entity; strips all tags; removes line breaks, tabs, and extra whitespace; and strips octets:

$str

The required string you want to sanitize.

sanitize_user( $username, $strict = false )

This function cleans a username of any illegal characters:

$username

A required string of the username to be sanitized.

$strict

An optional Boolean that, if set to true, will limit the username to specific characters.

sanitize_title( $title, $fallback_title = '' )

Sanitizes a title stripping out any HTML or PHP tags, or returns a fallback title for a provided string:

$title

A required string of the title to be sanitized.

$fallback_title

An optional string to use if the title is empty.

sanitize_email( $email )

Sanitizes an email address by stripping out any characters not allowed in an email address:

$email

The email address to be sanitized.

sanitize_file_name( $filename )

Sanitizes a filename, replacing whitespace with dashes. Removes special characters that are illegal in filenames on certain operating systems and special characters requiring special escaping to manipulate at the command line. Replaces spaces and consecutive dashes with a single dash. Trims period, dash, and underscore from beginning and end of filename:

$filename

Required string of the filename to be sanitized.

wp_kses( $string, $allowed_html, $allowed_protocols = array () )

This function makes sure that only the allowed HTML element names, attribute names, and attribute values plus only sane HTML entities will occur in the string you provide. You have to remove any slashes from PHP’s magic quotes before you call this function:

$string

A required string that you want filtered through kses.

$allowed_html

A required array of allowed HTML elements.

$allowed_protocols

An optional array of allowed protocols in any URLs in the string being filtered. The default allowed protocols are http, https, ftp, mailto, news, irc, gopher, nntp, feed, telnet, mms, rtsp, and svn. This covers all common link protocols, except for javascript, which should not be allowed for untrusted users.

wp_kses_post( $data )

This function sanitizes the data passed in, allowing the same HTML tags and protocols that are allowed in the post content section of the edit post page. If you want to use stricter rules than you would for authors, editors, and admins on your site, use the wp_kses() function with specific tags and protocols passed in. If your field is meant for administrators and you just want to sanitize a field the same way the post content is sanitized, the wp_kses_post() function is a good shortcut.

$data

A required string that you want filtered through kses.

The following code validates and sanitizes an email address:

// pretend a user added an email address "jason @ stranger$tudios.com"
$user_email = 'jason @ stranger$tudios.com';

// we can check if this is a valid email
$valid_email = is_email( $user_email );

// we know it's not because it's set to nothing from is_email()
if ( ! $valid_email )
	echo 'invalid email<br />';

// let's try again with sanitizing the email
$user_email = 'jason @ stranger$tudios.com';

// use sanitize_email() to try to fix any invalid email
$user_email = sanitize_email( $user_email );

$valid_email = is_email( $user_email );

if ( ! $valid_email )
	echo 'invalid email<br />';
else
	echo 'valid email: ' . $user_email;

Notice that in this example that the sanitize_email() function removes both the spaces and dollar sign in the invalid email. While the returned email address is technically valid, it’s not Jason’s real email address since the function doesn’t understand leet-speak well enough to swap the $ with an s. Also note that the returned value won’t always be a valid email address. If there is no @ sign, no text before the @ sign, or no domain behind the @ sign, then the returned value will be an invalid email.

Additional information on validating, sanitizing, and escaping data can be found in the WordPress Codex.

Nonces

Nonce means “number used once,” and using nonces is critical to protecting your application from cross-site request forgery (CSRF) attacks. Normally your server-side scripts for form processing are processing forms from your own site. People visit your site, log in, and submit a form to perform some action on your site. However, if your server-side code were simply looking for $_POST values to determine what to do, those values could be submitted from any form, even forms on other websites.

The first line of defense is to check that a user is really logged in and has the capabilities to do the requested action. However, this isn’t enough to stop CSRF attacks, because you might be logged in on your WordPress site (e.g., in a background tab) while some malicious code on another site/tab kicks off the form request with the correct $_POST variable to send a spammy message to your friends or initiate account deletion or something else you don’t want to do.

What’s needed is a way to make sure that the request comes from the WordPress site and not another site. This is what a nonce does. The basic outline of using a nonce is as follows:

  1. Generate a nonce string every time a page is loaded.

  2. Add the nonce string as a hidden element on the form.

  3. When processing a submitted form, generate the nonce the same way and check that it matches the one submitted from the form.

Because the nonce is generated using a combination of the secret salt keys in your wp-config.php and the server time, it is difficult for attackers to guess the nonce string for their spoofed forms.

Nonces are useful for nonform links and Ajax calls as well. The process is basically the same:

  1. Generate a nonce string every time a page is loaded.

  2. Add the nonce string as a parameter to the URL.

  3. When processing the request, generate the nonce the same way and check that it matches the one submitted through the URL.

Whether protecting your forms, links, or Ajax requests, WordPress has a few helper functions to make this process very easy to implement.

wp_create_nonce( $action = -1 )

This function will create a random token that can only be used once, and which is located in wp-includes/pluggable.php:

$action

An optional string or int that describes what action is being taken for the nonce created. You should always set an action to be more secure:

function schoolpress_footer_create_nonce(){
	$nonce = wp_create_nonce('random_nonce_action');
	$url = add_query_arg( array( 'sp_nonce' => $nonce ) );
	echo '<p><a href="' . $url . '">Verify this Nonce</a></p>';
}
add_action( 'wp_footer', 'schoolpress_footer_create_nonce' );

wp_verify_nonce( $nonce, $action = -1 )

This function is used to verify that the correct nonce was used within the allocated time limit. If the correct nonce is passed into this function and everything checks out OK, the function will return a value that evaluates to true.2 If not, it will return false. This function is located in wp-includes/pluggable.php:

$nonce

A required string of the nonce value being used to verify.

$action

An optional string or int that should be descriptive to what is taking place and should match the action from when the nonce was created.

function schoolpress_init_verify_nonce(){
  if ( isset( $_GET['sp_nonce'] )
    && wp_verify_nonce( $_GET['sp_nonce'], 'random_nonce_action' ) ) {
     echo 'You have a valid nonce!';
    } else {
     echo 'You have an invalid nonce!';
	}
}
add_action( 'init', 'schoolpress_init_verify_nonce' );

check_admin_referer( $action = -1, $query_arg = '_wpnonce’ )

This function calls the wp_verify_nonce() function, so it verifies nonces but also checks to see that the referrer, or the page that got you to the current page, is from the same website. This function is located in wp-includes/pluggable.php:

$action

An optional string, but you should specify a nonce action to be verified.

$query_arg

An optional string of the query argument that has the nonce as its value.

// checking the same nonce "sp_nonce" that was created earlier
function schoolpress_init_check_admin_referer(){
	if ( isset( $_GET['sp_nonce'] ) &&
	     check_admin_referer( 'random_nonce_action', 'sp_nonce' ) ) {
		echo '<p>You have a valid nonce!</p>';
	} else {
		echo '<p>You have an invalid nonce!</p>';
	}
}
add_action( 'init', 'schoolpress_init_check_admin_referer' );

wp_nonce_url( $actionurl, $action = -1 )

This function also utilizes the wp_create_nonce() function and adds a nonce to any URL. If you create any actions based off of a query string, you should always tie a nonce to your URL with this function:

$actionurl

A required string of the URL to which to add a nonce action.

$action

An optional string for the action name. You should always set this.

This function is located in wp-includes/functions.php:

// simple url with querystring example
function schoolpress_footer_nonce_url(){
	$url = wp_nonce_url(
		add_query_arg( array( 'action' => 'get_users' ) ),
		'get_users_nonce'
	);
	echo '<p><a href="' . esc_url( $url ) . '">Get Users</a></p>';
}
add_action( 'wp_footer', 'schoolpress_footer_nonce_url' );

// querystring action
function schoolpress_footer_nonce_url_action(){
	// check if querystring action is get_users and for the nonce
	if ( isset( $_GET['action'] )
		&& 'get_users' == $_GET['action']
		&& check_admin_referer( 'get_users_nonce' ) ) {
		echo 'Your action: ' . esc_html( $_GET['action'] );
		// or get your users and display them here...
	}
}
add_action( 'init', 'schoolpress_footer_nonce_url_action' );

Notice in this example that we used the esc_html() function when echoing the action that was passed into the query string. Normally, we haven’t been using the escape functions in our example code because they make it harder to read and understand what the code is doing. However, this is the security chapter and carelessly echoing URL parameters is one of the most common ways of introducing cross-site scripting vulnerabilities into your site.

Notice also, we use the esc_url() function when echoing the nonce URL into the link. Even when building a URL using the functions provided by WordPress, you must escape the URL before sending it to output. The esc_url() function will prevent itself from being run multiple times and ruining the URL.

wp_nonce_field( $action = -1, $name = ''_wpnonce'', $referer = true , $echo = true )

This function retrieves or displays a hidden nonce field in a form. The wp_create_nonce() function is baked into it, so you should always use this nice helper function when dealing with forms.

The nonce field is used to validate that the contents of the form came from the location on the current site and not somewhere else. The nonce does not offer absolute protection, but should protect against most cases. It is very important to use a nonce field in forms.

The $action and $name parameters are optional, but if you want to have better security, it is strongly suggested to set those two parameters. It is easier to just call the function without any parameters, because validation of the nonce doesn’t require any parameters, but since crackers know what the default is, it won’t be difficult for them to find a way around your nonce and cause damage.

The input name will be whatever $name value you gave. The input value will be the nonce creation value. This function is located in wp-includes/functions.php:

$action

An optional string for the action name. You should always set this.

$name

An optional string for the nonce name. You should always set this.

$referer

An optional Boolean of whether to set the referer field for validation. The default value is true.

$echo

An optional Boolean of whether to display or return a hidden form field. The default value is true.

<?php
// simple submission form example
function schoolpress_footer_form(){
	?>
	<form method="post">
	  <?php // create our nonce
	  wp_nonce_field( 'email_list_form', 'email_list_form_nonce' );
	  ?>
	  <h3>Join our email list</h3>
	  Email Address: <input type="text" name="email_address">
	  <input type="submit" name="submit_email" value="Submit" />
	</form>
	<?php
}
add_action( 'wp_footer', 'schoolpress_footer_form' );

// form action
function schoolpress_footer_form_action(){
	if ( isset( $_POST['submit_email'] )
	  && isset( $_POST['email_address'] )
	  && check_admin_referer( 'email_list_form',
	  'email_list_form_nonce' ) ) {
	  echo 'You submitted: ' . esc_html( $_POST['email_address'] );
	  // or process your form here...
	}
}
add_action( 'init', 'schoolpress_footer_form_action' );
?>

check_ajax_referer( $action = -1, $query_arg = false, $die = true )

When using Ajax, you should still be using nonces. This function allows you to do a nonce and referer check while processing an Ajax request. This function is located in wp-includes/pluggable.php:

$action

An optional string of the nonce action being referenced.

$query_arg

An optional string of where to look for nonce in $_REQUEST.

$die

An optional Boolean of whether you want the Ajax script to die if an invalid nonce is found.

Throughout the book, you may have noticed code snippets that didn’t use nonces or sanitize data. We did this to try to keep the code examples short and sweet, but you should always use nonces and sanitize your data. Any custom form submission or URL with custom query strings should utilize nonces, and every time you write $_POST['anything'] or $_GET['anything'], they should be wrapped in a sanitization or escaping function.

1 In technical terms, “super-duper long” is equal to about 4 GB of data.

2 The wp_verify_nonce() function will return 1 if the nonce is under 12 hours old. If the nonce is between 12 and 24 hours old, it will return 2. If it is older than 24 hours old, it will return false. This way you can test whether the result evaluates to true or, to check for a slightly fresher nonce, you could check if it is equal to 1 exactly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.219.217