WP Developer Security Mistakes
We all make mistakes, even experienced developers. Unfortunately, an innocent mistake by a developer can lead to catastrophic consequences. That is why it is important to create a security checklist and review your code against it as part of the development process. Here is a list of common security mistakes WordPress developers make.
Showing Too Much Information
Mistake #1: Allowing Visitors To Directly Execute A PHP File
WordPress is built on the backend programming language PHP as you may know. When a developer creates a theme or plugin they assume that WordPress is going to load before their PHP files are executed. Interestingly enough, a hacker can attempt to run a specific PHP file directly by accessing it in the browser or remotely and can send information to the specific file using the GET or POST method.
Most of the time, an error will occur and the PHP process will fail as it may be relying on functions or methods that are not defined yet due to WordPress not getting loaded first. In some cases, the file could run a script fully or partially and cause the program to operate in a way the developer did not intend. The outcome depends on what the script within the file is attempting to do. This could lead to disclosing sensitive information or possibly modifying files or the database without the proper access.
If the script were to create a new user, delete files, or even alter or reset settings, this vulnerability becomes a serious threat to the site. Of course, when these types of actions are being done, there should be a check to ensure the user has the proper access to perform them. Furthermore, there should be a check to ensure WordPress has been loaded first.
Easy Solution
Add the following line of code to the top of each PHP file to ensure that the WordPress core files are loaded first.
<?php
// Prevent Direct Access
if ( ! defined( 'ABSPATH' ) ) { die; }
The constant variable ABSPATH is defined in the wp-config.php file when WordPress loads. Constant variables cannot be altered once they have been set. If the variable has not been set, then the script above will kill the PHP process and nothing will happen.
Mistake #2: Allowing Directory Browsing
By default, if someone accesses a directory on a website, all the files within that directory will be shown to the user allowing them to browse through files on the server. This in itself is not a big deal unless there are sensitive files within that directory that should not be public.
To prevent this from happening you have a few options:
- Place an empty index.html file in each directory. If a user visits that directory directly, then the index.html file will be served and a blank page will be shown.
- Disable directory browsing at the hosting server level. If your server is running Apache and CPanel, you can disable it with these instructions, otherwise, you’ll have to do it the old fashion way. An NGINX web server has directory browsing disabled by default.
- Disable using .htaccess. If you do not have root access, you can always add the following line of code into the .htaccess file in the root of the WordPress installation directory.
# Disable Directory Browsing Options All -Indexes
Mistake #3: Ignoring PHP Notices / Warnings / Errors
PHP errors are logged by the server as a way for developers to take notice of flaws in their code. Oftentimes, these errors may be the sign that the website is not functioning properly, or will no longer be functioning properly in future PHP versions.
If a PHP error occurs, depending on the severity of the error, it may not finish executing the remainder of the script. This could cause the website to break in a way that exposes sensitive information to the public or place the site in a vulnerable state.
Solution
Clone the live site to create a staging or development site. Make sure that the database is a clone and that wp-config.php file is referencing the cloned database and not referencing the live site’s database. Go into wp-config.php and there should be a section of PHP code that looks like this:
define('WP_DEBUG', false);
And change it to this:
// Enable WP_DEBUG mode
define( 'WP_DEBUG', true );
// Enable Debug logging to the /wp-content/debug.log file
define( 'WP_DEBUG_LOG', true );
// Disable display of errors and warnings
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
Now browse through the site and then check the /wp-content/debug.log file for errors. You could also enable the display of errors on the screen to make things a little more convenient.
Notice: If your WordPress site uses Ajax requests or Rest-API requests, then the errors will not be displayed on the screen if they happen during that request. They will appear in the response of the request which can be accessed via the inspect panel in the Chrome browser [1].
Learn about debugging in WordPress.
Mistake #4: Leaving Error Display / Logging Active For A Live Site
It is helpful to enable error logging to troubleshoot and fix problems as shown above. It is important to turn off error logging for multiple reasons:
- You do not want your errors displayed on the screen as it will make the website appear broken.
With error logging on, the debug.log file will continue to grow in size. If the website gets a substantial amount of traffic, this file could grow in size rapidly and eventually max out your hosting disk space. - If your errors are publicly viewable, then it exposes flaws in your code that could be used to circumvent security measures.
- If you have error logging on, that debug.log file can be downloaded by a hacker as it is a publicly available file.
Solution
- Turn off WP Debug, error logging, and error display.
- Delete the /wp-content/debug.log file so that no one can publicly view this file.
Sanitizing Data
Mistake #5: Using $_SERVER Super Global Variables Instead of Relying On WP
The PHP super global $_SERVER is an array of values set by the server [2]. This array of values can be unreliable as it is the server’s responsibility to set these values and each server may be configured differently leaving some information missing [3].
Also, if you are relying on a superglobal, then it is not 100% safe. You can sanitize the data being received, or you can request the same data from WordPress if it is available.
Unsecure Way
<!-- Unreliable and Unsafe Reference -->
<a href="<?= $_SERVER['HTTP_HOST']; ?>">Home</a>
Warning: Do not use the code example above. Using it will place your site at a high risk of getting hacked.
Secure Way – Using WordPress API
<!-- Reliable and Safe Reference -->
<a href="<?= get_site_url(); ?>">Home</a>
In the first example, the developer is referencing the PHP superglobal $_SERVER. The $_SERVER[‘HTTP_HOST’] and other variables can be changed by sending a different Host header when accessing the site with a curl request [4]. Using these variables carelessly can expose your site to attacks such as cache poisoning and phishing attacks [5].
Mistake #6: Using Unsanitized WP Functions
When you are interacting with the WordPress database, it makes sense to use the WordPress API instead of creating your own custom functions. The main benefits are convenience and sanitization, but you need to know which functions are sanitized and which functions are not.
Unsanitized WordPress Function
- $wpdb->query() – This function gives you the ability the run custom database queries, but it does not pass through any sanitization making it unsafe by itself [6][7].
// Example of vulnerably usage $wpdb->query( " UPDATE $wpdb->posts SET post_title = '$newtitle' WHERE ID = $my_id ");
Warning: Do not use the code example above. Using it will place your site at a higher risk of getting hacked. Please refer to the section about using $wpdb->prepare() to sanitize properly.
Partially Sanitized Functions
- wp_update_post() – This function is sanitized by wp_slash() if passing an object variable. Arrays need to be sanitized prior to using this function [8].
Warning: If you pass a variable as an array to wp_update_post(), it will not get sanitized [9].
Most Popular Sanitized WordPress Functions
We have only listed the most popular sanitized functions below. There are certainly many more safe functions available in the WordPress codex.
- $wpdb->prepare() – Use this function to sanitize your query before using $wpdb->query() [10][11].
// Example of proper usage of prepare() and query() together $sql = $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_name = %s OR ID = %d", $some_name, $some_id ); $results = $wpdb->query( $sql );
- $wpdb->insert() – Safely insert data into a specific or custom table [12][13].
// Example of inserting a row in the database safely $wpdb->insert( $table, $data );
Notice: We recommend that you use wp_insert_post() instead unless you are handling a custom database table. Using wp_insert_post() will not only sanitized the input data, but it also fires all subsequent hooks during the process.
- $wpdb->update() – Use this function to safely update data in a specific table [14][15].
// Example of how to update a table safely $wpdb->update( $wpdb->posts, ['post_title' => $new_title ], // Data as array ['ID' => $my_id ] // Where condition as array );
Notice: We recommend that you pass an object variable through wp_update_post() so that it is not only sanitized, but also fires all subsequent hooks during the process.
- wp_insert_post() – Safely insert a new post. This function passes data through sanitize_post(), which handles all the sanitization and validation [16].
- add_metadata() – This function uses sanitize_key(), wp_unslash(), and absint() to sanitize the data before adding the metadata [17].
Notice: We recommend that you do not use add_metadata() directly as there are wrapper functions below that are best suited for each specific purpose to add better context to your codebase.
- add_post_meta() – Safely add post metadata for a post. This function sends data through add_metadata() [18][19].
- add_user_meta() – Safely add metadata to a user. This function sends data through add_metadata() [20][21].
- add_term_meta() – Safely add metadata to a term. This function sends data through add_metadata() [22][23].
- add_site_meta() – Safely add metadata to the site. This function sends data through add_metadata() [24][25].
- update_metadata() – This function uses wp_unslash(), sanitize_key(), and absint() to sanitize the data before updating the metadata [26].
Notice: We recommend that you do not use update_metadata() directly as there are wrapper functions below that are best suited for each specific purpose to add better context to your codebase.
- update_post_meta() – Safely update post metadata. This function sends data through update_metadata() [27][28].
- update_user_meta() – Safely update user metadata. This function sends data through update_metadata() [29][30].
- update_term_meta() – Safely update term metadata. This function sends data through update_metadata() [31][32].
- update_site_meta() – Safely update the site’s metadata. This function sends data through update_metadata() [33][34].
Migrating Websites
Mistake #7: Backing Up Files With The Wrong File Extension
Sometimes, a developer will need to backup a specific file to restore the old code quickly in case something breaks. The easiest way to do this is to copy the file and rename it as something else. The most common mistake is to use a file extension that does not exist.
For Example
If you need to make some major changes to wp-config.php, you may want to backup the file by duplicating it and naming it to one of the following:
- wp-config.php.bak
- wp-config.bak
- wp-config.old
Problem
The file wp-config.php contains sensitive information within it such as the database name, username, and password. If someone names a file with an extension of “.bak”, “.old”, or anything other than “.php”, the contents of that file will be likely processed as plain text and made public to anyone that attempts to access the file.
Solution
Name the backup file like this: [name-of-file]-[date].php
In the scenario above, we would have named the backup file this: wp-content.php-03012020.php
The backup file is named in this way to provide ease when renaming the file back to its original name quickly. The first instance of “.php” will be recognized as part of the filename string and the last occurrence of .php will designate this file as a PHP file. Since the file is a PHP file, if the file is directly accessed, it will be processed as PHP by the server and none of the sensitive information will be shown to the user.
Mistake #8: Forget To Delete Downloadable Backups
When a website is migrated from one server to the next, all of the files get compressed into one single archive file. If a graphical user interface file manager is used to create this archive file, the file usually gets automatically named the same as the first file’s name and then the archive extension gets added (.zip, .tar, .tar.gz, .bz, .7z, etc.). This file is then moved to the new server and then uncompressed into the root of the website.
Problem
Since the backup file is in the root of the website, it can be downloaded by anyone that guesses the filename. In most cases, the filename is very predictable. That backup file contains the wp-config.php file which contains the database name, username, and password needed to access your database.
Warning: If someone downloads a copy of your backup archive file, they will have access to a copy of wp-config.php. Then they can remotely connect and alter your database.
Solution
Delete all archive files once they have served their purpose.
Mistake #9: Bad File Permissions
As mentioned above, a website migration from one server to another involves creating an archive file containing all your website’s files and file structure. When you decompress the archive file, it is likely that your file permissions did not carry over to your new location. This can depend on the type of compression used during the backup process.
Problem
If your file permissions are not properly set, then the website might not load or the public may have write access to your files.
Solutions
- Set all of the permissions for directories recursively to 0755.
- Set all the permissions for files recursively to 0644.
- Set wp-config.php to 0600, which prevents public viewing.
- Set any other files that may leak information about your site to 0600.