Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Building a WordPress Security Plugin: The Basics

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

WordPress is open source, which means that everyone, including hackers with a malicious intent, can scour the source code looking for holes in its security. In this set of tutorials, we will be going through the process of creating a WordPress plugin to detect and fix any security flaws that may lie in your WordPress installation.


Step 1 Setup & Metadata

Go ahead and make a new folder in your plugins directory called safe, then create a file called safe.php inside it. The first few lines of code for a plugin are just telling WordPress that it is a plugin, what it's called and other information of that nature. This section is similar to the metadata of a theme or template.

/*
Plugin Name: Safe
Plugin URI: http://wp.tutsplus.com

Description: The easiest, most effective way to secure your WordPress site from attackers.
Author: Fouad Matin
Version: 1.0
Author URI: http://wp.tutsplus.com/author/fouad
*/

Update: As per Christopher's comment below, there is no longer any need to define WP_CONTENT_URL, WP_CONTENT_DIR, WP_PLUGIN_URL, and WP_PLUGIN_DIR as WordPress defines them for us in the file includes/default-constants.php.


Step 2 Action & Filter Systems

Next we have to put in place some action hooks, which are functions triggered after a certain event happens, such as the admin_menu function being called. Actions are typically used for modifying database data, email messages, and modifying what is displayed on the screen. Filters, on the other hand, are hooks that are launched prior to rendering text or sending data. You can also remove actions if you want, such as the wp_generator function.

remove_action('wp_head', 'wp_generator');

Step 3 Define the Actual Functions

First, before adding any of our text into the plugin, we are going to essentially embed a stylesheet using the wp_enqueue_style function. This function is just a safe way to add CSS files to a WordPress generated page. For more information on enqueuing, check out Japh's tutorial on including javascript and css.

add_action('admin_enqueue_scripts','safe_styles');
function safe_styles(){
	wp_enqueue_style('safe_style', plugins_url('/css/safe.css', __FILE__));
}

Now, in order for the user to be able to open the plugin page, we need to add a menu page for our plugin. Also, the security plugin should only be available to administrators (you can change this if you want).

add_action('admin_menu', 'add_menu_bpg');
function add_menu_bpg()
{
	if (!current_user_can('administrator'))
	{
		return false;
	}

	if (function_exists('add_menu_page'))
	{
		add_menu_page('Safe', 'Safe', 'edit_pages', 'safe-admin-page', 'safe_main', WP_PLUGIN_URL.'/safe/img/safe.png');
	}
}

Step 4 Metaboxes

In order to display information on the plugin page, we use metaboxes to make it look consistent with the WordPress admin theme. We are going to want to display the system/server information and also run some basic, quick security checks.

function safe_meta_box()
{
?>
	<div id="safe-basic-checks" class="safe-inside">
		<div class="safe-basic-checks-section"><?php safe_check_version();?></div>

		<div class="safe-basic-checks-section"><?php safe_check_table_prefix();?></div>

		<div class="safe-basic-checks-section"><?php safe_version_removal();?></div>

		<div class="safe-basic-checks-section"><?php safe_errorsoff();?></div>

		<div class="safe-basic-checks-section"><?php safe_wpConfigCheckPermissions('/wp-config.php');?>
	</div>
<?php

Now, let's check if there is an account with the default setting of admin which is the first guess of most hackers and therefore an easy way into your admin panel. Also, I recommend you configure an .htaccess file in your wp-admin directory that blocks all IP addresses other than yours. In our next article, we will set up a way to configure the .htaccess from the plugin page.

	global $wpdb;

	echo '<div class="pass">WP ID META tag removed form WordPress core</div>';

	echo '<div class="safe-basic-checks-section">';

		$name = $wpdb->get_var("SELECT user_login FROM $wpdb->users WHERE user_login='admin'");
		if ($name == "admin")
		{
			echo '<span class="fail">"admin" user exists.</span>';
		}
		else {
			echo '<span class="pass">No user "admin".</span>';
		}

	echo '</div>';

	echo '<div class="safe-basic-checks-section">';
		if (file_exists('.htaccess'))
		{
			echo '<span class="pass">.htaccess file found in wp-admin/</span>';
		}
		else {
			echo '<span class="fail">The file .htaccess does not exist in the wp-admin section.</span>';
		}
	echo '</div>';

?>
	</div>
<?php
}

Let's add another column/box for the right side of the screen, where we can display all of the server's information and settings.

function safe_meta_box2()
{
?>
	<ul id="safe-information-scan-list">
		<?php safe_get_serverinfo(); ?>
	</ul>
<?php
}

Step 5 Main Functions

Just for organizational purposes, create a new directory named inc inside your safe directory, where you will create a file called functions.php to hold our main functions. These functions will be responsible for running the tests, checking file permissions, database settings, etc. It's better just to separate these from the main plugin file so that it's easier to navigate and read.

First, start off by initiating a connection to the database and retrieving the MySQL version and MySQL information array. Also, let's check to see if PHP safe mode is turned on, as it may cause some headaches with other plugins and settings.

function safe_get_serverinfo() {
	global $wpdb;
	$sqlversion = $wpdb->get_var("SELECT VERSION() AS version");
	$mysqlinfo = $wpdb->get_results("SHOW VARIABLES LIKE 'sql_mode'");
	if (is_array($mysqlinfo)) {
		$sql_mode = $mysqlinfo[0]->Value;
	}
	if (empty($sql_mode)) {
		$sql_mode = __('Not set');
	}
	$sm = ini_get('safe_mode');
	if (strcasecmp('On', $sm) == 0) { 
		$safe_mode = __('On'); 
	}
	else { 
		$safe_mode = __('Off'); 
	}

In order to accept incoming pingbacks, allow_url_fopen has to be set to "on" in your php.ini. However, it also makes your code susceptible to code injection, because the file_get_contents() can retrieve data from remote locations such as an FTP or web server.

	if(ini_get('allow_url_fopen')) {
		$allow_url_fopen = __('On');
	}
	else {
		$allow_url_fopen = __('Off');
	}

Memory limitations and execution times will play a major role in the next part of the tutorial for the plugin to suggest possible solutions to security flaws while taking the system's limits into account.

	if(ini_get('upload_max_filesize')) {
		$upload_max = ini_get('upload_max_filesize');
	}
	else {
		$upload_max = __('N/A');
	}
	if(ini_get('post_max_size')) {
		$post_max = ini_get('post_max_size');
	}
	else {
		$post_max = __('N/A');
	}
	if(ini_get('max_execution_time')) {
		$max_execute = ini_get('max_execution_time');
	}
	else {
		$max_execute = __('N/A');
	}
	if(ini_get('memory_limit')) {
		$memory_limit = ini_get('memory_limit');
	}
	else {
		$memory_limit = __('N/A');
	}
	if (function_exists('memory_get_usage')) {
		$memory_usage = round(memory_get_usage() / 1024 / 1024, 2) . __(' MByte');
	}
	else {
		$memory_usage = __('N/A');
	}
	if (is_callable('exif_read_data')) {
		$exif = __('Yes'). " ( V" . substr(phpversion('exif'),0,4) . ")" ;
	}
	else {
		$exif = __('No');
	}
	if (is_callable('iptcparse')) {
		$iptc = __('Yes');
	}
	else {
		$iptc = __('No');
	}
	if (is_callable('xml_parser_create')) {
		$xml = __('Yes');
	}
	else {
		$xml = __('No');
	}

Now, we just spit out all of the data that we just collected about the server.

?>
	<li><?php _e('Operating System'); ?> : <strong><?php echo PHP_OS; ?></strong></li>
	<li><?php _e('Server'); ?> : <strong><?php echo $_SERVER["SERVER_SOFTWARE"]; ?></strong></li>
	<li><?php _e('Memory usage'); ?> : <strong><?php echo $memory_usage; ?></strong></li>
	<li><?php _e('MYSQL Version'); ?> : <strong><?php echo $sqlversion; ?></strong></li>
	<li><?php _e('SQL Mode'); ?> : <strong><?php echo $sql_mode; ?></strong></li>
	<li><?php _e('PHP Version'); ?> : <strong><?php echo PHP_VERSION; ?></strong></li>
	<li><?php _e('PHP Safe Mode'); ?> : <strong><?php echo $safe_mode; ?></strong></li>
	<li><?php _e('PHP Allow URL fopen'); ?> : <strong><?php echo $allow_url_fopen; ?></strong></li>
	<li><?php _e('PHP Memory Limit'); ?> : <strong><?php echo $memory_limit; ?></strong></li>
	<li><?php _e('PHP Max Upload Size'); ?> : <strong><?php echo $upload_max; ?></strong></li>
	<li><?php _e('PHP Max Post Size'); ?> : <strong><?php echo $post_max; ?></strong></li>
	<li><?php _e('PHP Max Script Execute Time'); ?> : <strong><?php echo $max_execute; ?>s</strong></li>
	<li><?php _e('PHP Exif support'); ?> : <strong><?php echo $exif; ?></strong></li>
	<li><?php _e('PHP IPTC support'); ?> : <strong><?php echo $iptc; ?></strong></li>
	<li><?php _e('PHP XML support'); ?> : <strong><?php echo $xml; ?></strong>  </li>
<?php
}

Another major flaw in most generic WordPress installs is the use of the default table prefix: wp_. In our next article, we will set up a way to backup and rename the tables which is where the safe_check_table_prefix and safe_errorsoff functions will come into play.

function safe_check_table_prefix() {
	if($GLOBALS['table_prefix']=='wp_') {
		echo '<span class="fail">Your table prefix should not be <em>wp_</em>.</span><br />';
	}
	else {
		echo '<span class="pass">Your table prefix is not <i>wp_</i>.</span><br />';
	}
}

function safe_errorsoff() {
	echo '<span class="pass">WordPress DB Errors turned off.</span><br />';
}

If you have an older version of WordPress, then first upgrade. Regardless of how good you are of how keeping track of upgrades, there is always the possibility that you might fall behind. Once the security flaws of a previous version are listed in the change log for the new version, your installation's security is now compromised. Most themes do it by default but just to be sure, we removed it earlier using the remove_action filter and now we'll just make sure the end-user knows too.

function safe_version_removal() {
	global $wp_version;
	echo '<span class="pass">Your WordPress version is successfully hidden.</span><br />';
}

function safe_check_version() {
	$c = get_site_transient( 'update_core' );
	if ( is_object($c)) {
		if (empty($c->updates)) {
			echo '<span class="pass">'.__('You have the latest version of Wordpress.').'</span>';
			return;
		}

		if (!empty($c->updates[0])) {
			$c = $c->updates[0];

			if ( !isset($c->response) || 'latest' == $c->response ) {
				echo '<span class="pass">'.__('You have the latest version of Wordpress.').'</span>';
				return;
			}

			if ('upgrade' == $c->response) {
				$lv = $c->current;
				$m = '<span class="fail">'.sprintf('Wordpress <strong>(%s)</strong> is available. You should upgrade to the latest version.', $lv).'</span>';
				echo __($m);
				return;
			}
		}
	}

	echo '<span class="fail">'.__('An error has occurred while trying to retrieve the status of your Wordpress version.').'</span>';
}

With the newer versions of WordPress, the security of the wp-config.php shouldn't be a problem, but it's better to be safe than sorry. If someone with malicious intent were to get their hands on your wp-config.php, they could access your database access credentials, authorization keys and database salts. We'll just check if we can get the file, write to the file or if it exists in that directory at all. If it does, we'll show an urgent notice and, in our next article, a method to fix the issue easily.

function safe_wpConfigCheckPermissions($wpConfigFilePath) {
	if (!is_writable($wpConfigFilePath)) { 
		echo '<span class="pass">'.__('Your wp config file is not a threat.').'</span>';
		return false; 
	}

	if (!function_exists('file') || !function_exists('file_get_contents') || !function_exists('file_put_contents')) {
		echo '<span class="pass">'.__('Your wp config file is not a threat.').'</span>';
		return false;
	}
	else {
		echo '<span class="fail">'.__('Your wp config file can be compromised by hackers, fix the permissions.').'</span>';
		return true;
	}
}

Now, we have to go back to the safe.php file and add our new functions.php, above the action and filter hooks.

require_once(WP_PLUGIN_DIR . "/safe/inc/functions.php");

Step 6 Rendering

Back to the safe.php, the last addition is the safe_main() function which is called when the page is loaded. Here we need to add the metaboxes we defined earlier and add some styling.

function safe_main() {

	add_meta_box("safe_box_1", 'Basic Checks', "safe_meta_box", "box1");
	add_meta_box("safe_box_2", 'System Information', "safe_meta_box2", "box2");

	echo '
		<div class="metabox-holder">
		<div style="float:left; width:48%;" class="inner-sidebar1">';

	do_meta_boxes('box1','advanced','');

	echo '
		</div>
		<div style="float:right;width:48%;" class="inner-sidebar1">';
		do_meta_boxes('box2','advanced','');

	echo '
		</div>

		<div style="clear:both"></div>
		</div>';

}

Lastly, make a CSS file named safe.css in a directory called css under our safe directory. You can style the boxes however you want, or you can just use the following CSS code for the error and success text:

div.pass,span.pass {
	color:#7AB317;
}
div.fail,span.fail {
	color:#ff3333;
}
div.fail,span.fail a{
	color:#ff3333;
}

Conclusion

By now, you should know how to add a menu page for your plugin, apply action and filter hooks, and programatically check for any security flaws in WordPress.

If you have any additional suggestions or questions regarding security plugins, feel free to leave a comment! Also, this plugin is on GitHub, so go ahead, fork the repository and customize it however you want.

Update: this article has been updated as per Christopher's suggestions below.

Advertisement