Capabilities and Nonces

In this previous article I looked at helping keep your theme or plug-in secure through appropriate data sanitization and validation. In this article, we’ll be looking at another important aspect of WordPress security: capabilities and nonces.

When developing a plug-in (and to a lesser extent, themes) you will often find that you want to allow a user to perform various actions: delete, edit or update posts, categories, options or even other users. More often than not you only want certain authorised users to perform these actions. For this, WordPress uses two concepts: roles and capabilities.


Front-End Delete Link: A Simple Example

Let’s suppose we want a front-end delete button to quickly remove posts. The following creates a link wherever we use wptuts_frontend_delete_link() inside the loop.

function wptuts_frontend_delete_link() {
	$url = add_query_arg(
		array(
			'action'=>'wptuts_frontend_delete',
			'post'=>get_the_ID();
		)
	);

	echo  "<a href='{$url}'>Delete</a>";
}

Then to process the delete action:

if ( isset($_REQUEST['action']) && $_REQUEST['action']=='wptuts_frontend_delete' ) {
	add_action('init','wptuts_frontend_delete_post');
}

function wptuts_frontend_delete_post() {

	// Get the ID of the post.
	$post_id = (isset($_REQUEST['post']) ?  (int) $_REQUEST['post'] : 0);

	// No post? Oh well..
	if ( empty($post_id) )
		return;

	// Delete post
	wp_trash_post( $post_id );

	// Redirect to admin page
	$redirect = admin_url('edit.php');
	wp_redirect( $redirect );
	exit;
}

Then when a user clicks the ‘delete’ link, the post is trashed and the user is redirected to the post admin screen.

The problem with the above code is that it performs no permission checks: anyone can visit the link and delete a post – not only that, but by changing the post query variable they can delete any post. First of all, we want to make sure that only people who we want to be able to delete posts are able to delete posts.


Permissions, Roles and Capabilities

When a user is registered on your WordPress site they are assigned a role: this might be admin, editor or subscriber. Each role is assigned capabilities, for example edit_posts, edit_others_posts, delete_posts or manage_options. Whichever role a user is assigned, they inherit those capabilities: capabilities are assigned to roles, not users.

These capabilities dictate which parts of the admin screen they can access, and what they can and cannot do while there. It’s important to note that when checking permissions you check the capability, and not the role. Capabilities can be added or removed to roles, and so you cannot assume that an ‘admin’ user should be able to manage the site’s options – you need to specifically check if the current user actually has the manage_options capability.

For instance, generally you should avoid:

if ( current_user_can('admin') ) {
	// Do something that only users who can manage options should be able to do.
}

Instead, check the capability (or capabilities):

if ( current_user_can('manage_options') ) {
	// Do something that only users who can manage options should be able to do.
}

Add/Remove Capabilities

Adding and removing capabilities is very simple. WordPress provides the add_cap and remove_cap methods for the WP_Role object. For instance to add the capability ‘perform_xyz’ to the editor role:

	$editor = get_role('editor');
	$editor->add_cap('perform_xzy');

Similarly to remove a capability:

	$editor = get_role('editor');
	$editor->remove_cap('perform_xzy');

A role’s capabilities are stored in the database – so you only need to perform this once (for instance when your plug-in is activated or uninstalled).

If you want your plug-in to provide settings to allow users to edit other’s capabilities (capabilities that relate to your plug-in’s functionality) then a useful function is to use get_editable_roles() which returns a filtered array of roles. This shouldn’t be used instead of permission checks, but does allow your plugin user to restrict which roles can be edited by any given role. For instance, editors might be allowed to edit users – but only authors.

Meta Capabilities

The capabilities we’ve seen so far called ‘primitive’ capabilities – and these are assigned to various roles. Then there are meta capabilities, which are not assigned to roles, but instead map to primitive capabilities which are required of the current user’s role. For instance, given a post ID – we might want to ask does a user have the capability to edit this post?

if ( current_user_can('edit_post',61) ) {
	// Do something that only users who can edit post 61 should be able to do.
}

The edit_post capability isn’t assigned to any role (the primitive capability, edit_posts, however, is) – instead, WordPress checks which primitive roles this user requires in order to grant them permission to edit this post. For instance, if the current user is the post author they require the edit_posts capability. If they are not, they require the edit_others_posts capability. In both cases, if the post is published, they will also require the edit_published_posts capability. In this way meta capabilities are mapped to one or more primitive capabilities.

When you register a post type, by default the capabilities that are checked are the same as that for posts. However, you can specify your own capabilities:

register_post_type(
	'event',
	array(
		...
		'capabilities'=> array(
			// Meta capabilities
			'edit_post'=> 'edit_event',
			'read_post'=> 'read_event',
			'delete_post'=> 'delete_event',

			// Primitive capabilities
			'edit_posts'=> 'edit_events',
			'edit_others_posts'=> 'edit_others_events',
			'publish_posts'=> 'edit_others_events',
			'read_private_posts'=> 'read_private_events',
		),
		...
	)
);

Then to check if the current user has the permission to edit posts:

if ( current_user_can('edit_events') ) {
	// Do something that only users who can edit events should be able to do.
}

and to check if the current user can edit a particular event:

if ( current_user_can('edit_event',$post_id) ) {
	// Do something that only users who can edit $post_id should be able to do.
}

However – edit_event (like read_event and delete_event) is a meta capability, and so we need to map into the relevant primitive capabilities. To do so we use the map_meta_cap filter.

The logic is explained in the comments, but essentially we first check that the meta capability relates to our event post type, and that the passed post ID refers to an event. Next, we use a switch statement to deal with each meta capability and add roles to the $primitive_caps array. It’s these capabilities that the current user will need if they are to be granted permission – and exactly what they are dependant on the context.

add_filter('map_meta_cap', 'wptuts_event_meta_cap',10,4);

function wptuts_event_meta_cap( $primitive_caps, $meta_cap, $user_id, $args ) {
	// If meta-capability is not event based do nothing.
	if ( !in_array($meta_cap,array('edit_event','delete_event','read_event') ) {
		return $primitive_caps;
	}

	// Check post is of post type.
	$post = get_post( $args[0] );
	$post_type = get_post_type_object( $post->post_type );
	if ( 'event' != $post_type ) {
		return $primitive_caps;
	}

	$primitive_caps = array();

	switch( $meta_cap ):
		case 'edit_event':
			if ( $post->post_author == $user_id ) {
				// User is post author
				if ( 'publish' == $post->post_status ) {
					// Event is published: require 'edit_published_events' capability
					$primitive_caps[] = $post_type->cap->edit_published_posts;

				}
				elseif ( 'trash' == $post->post_status ) {
					if ('publish' == get_post_meta($post->ID, '_wp_trash_meta_status', true) ) {
						// Event is a trashed published post require 'edit_published_events' capability
						$primitive_caps[] = $post_type->cap->edit_published_posts;
					}

				}
				else {
					$primitive_caps[] = $post_type->cap->edit_posts;
				}

			}
			else {
				// The user is trying to edit a post belonging to someone else.
				$caps[] = $post_type->cap->edit_others_posts;

				// If the post is published or private, extra caps are required.
				if ( 'publish' == $post->post_status ) {
					$primitive_caps[] = $post_type->cap->edit_published_posts;

				}
				elseif ( 'private' == $post->post_status ) {
					$primitive_caps[] = $post_type->cap->edit_private_posts;
				}
			}
			break;

		case 'read_event':
			if ( 'private' != $post->post_status ) {
				// If the post is not private, just require read capability
				$primitive_caps[] = $post_type->cap->read;

			}
			elseif ( $post->post_author == $user_id ) {
				// Post is private, but current user is author
				$primitive_caps[] = $post_type->cap->read;

			}
			else {
				// Post is private, and current user is not the author
				$primitive_caps[] = $post_type->cap->read_private_post;
			}
			break;

		case 'delete_event':
			if ( $post->post_author == $user_id  ) {
				// Current user is author, require delete_events capability
				$primitive_caps[] = $post_type->cap->delete_posts;

			}
			else {
				// Current user is no the author, require delete_others_events capability
				$primitive_caps[] = $post_type->cap->delete_others_posts;
			}

			// If post is published, require delete_published_posts capability too
			if ( 'publish' == $post->post_status ) {
				$primitive_caps[] = $post_type->cap->delete_published_posts;
			}
			break;

	endswitch;

	return $caps;
}

Returning to our front-end delete link, we want to add the following capability check. Add this just above the wp_trash_post call in wptuts_frontend_delete_post.

if ( ! current_user_can('delete_post',$post_id) )
	return;

Nonces

The above capability check ensures that only users who have permission to delete that post, are able to delete that post. But suppose someone tricks you into visiting that link. You have the necessary capabilities, so you unwittingly delete the post. Clearly we need to check that the current user intends to perform the action. We do this through nonces.

The analogy is that an attacker wants to hand some instructions to someone. Capability checks is the receiver demanding to see some ID first. But what if the attacker slips the instructions into your hand? The receiver will gladly carry them out (you, after all, have permission to make such instructions).

A nonce is like a seal on an envelope that verifies you were the actual sender. The seal is unique to each user, so if the attacker slipped those instructions into your hand, the receiver could inspect the seal and see it isn’t yours. However, seals can be forged – so a nonce changes every time you hand over instructions. This seal is ‘for the nonce‘ (hence the name) or in other words, temporary.

So if someone sends you the delete link, it will contain their nonce and so will fail the nonce check. Usually nonces are one use only, but WordPress’ implementation of nonces is slightly different: the nonce in fact changes every 12 hours, and each nonce is valid for 24 hours. You can change this with the nonce_life filter that filters the a nonce’s life in seconds (so normally 86400)

add_filter('nonce_life', 'wptuts_change_nonce_hourly');
function wptuts_change_nonce_hourly( $nonce_life ) {
	// Change nonce life to 1 hour
	return 60*60;
}

(but 24 hours should be sufficiently secure). More importantly, nonces should be unique to the instructions themselves, and any objects they relate to (e.g. deleting a post, and the post ID).

How Nonces Generated

WordPress takes a secret key (you can find it in your config file) and hashes it along with the following parts:

  • action – this uniquely identifies the action. This includes the action, and if applicable, the object ID you are applying the action to: e.g. for deleting the post with ID 61 we might set the nonce action to be wptuts_frontend_delete_61
  • user ID – ID which identifies the user ID. This makes the nonce unique to each user.
  • tick – The ‘tick’ marks progress in time. It increments every 12 hours (or half of what the nonce life is). This makes the nonce change every 12 hours.

To create a nonce, you can use wp_create_nonce($action) where $action is explained above. WordPress then appends the tick and user ID and hashes it with the secret key.

You then send this nonce along with the action and any other data you need to perform that action. Checking the nonce is very simple.

	// $nonce is the nonce value received with the action. $action is what we used to generate the nonce
	wp_verify_nonce($nonce, $action); // Returns true or false

where $nonce is the received nonce value and $action is the requested action as above. WordPress then generates the nonce using the $action and checks if it matches the given $nonce variable. If someone had sent you the link, their nonce will have been generated with their ID and so will be different to yours.

Alternatively, if the nonce was posted or added as a query variable, with name $name:

	check_admin_referer($action, $name);

If the nonce is invalid it will stop any further action and display a ‘Are you sure?‘ message.

WordPress makes it especially easy to use nonces: For forms you can use wp_nonce_field($action,$name). This generates a hidden field with name $name and the nonce generated form $action as its value.

For URLs you can use wp_nonce_url($url,$action). This takes the given $url and returns it with the the query variable _wpnonce added, with the generated nonce as its value.

In our example:

function wptuts_frontend_delete_link() {
	$url = add_query_arg(
		array(
			'action' => 'wptuts_frontend_delete',
			'post' => get_the_ID();
		)
	);

	$nonce = 'wptuts_frontend_delete_' . get_the_ID();

	echo  "<a href='".wp_nonce_url($url,$nonce)."'>Delete</a>";
}

Which (for the post with ID 61) generates a nonce with action wptuts_frontend_delete_61. Then just above the trash call in wptuts_frontend_delete_post, we can check the nonce:

	check_admin_referer('wptuts_frontend_delete_'.$post_id, '_wpnonce');

Using Nonces in AJAX Requests

Using nonces in AJAX requests is slightly more involved. Nonces are generated server side, so the nonce value needs to printed as a javascript variable to be sent along with the AJAX request. To do this you can use wp_localize_script. Let’s suppose you have registered a script called wptuts_myjs which contains an AJAX request.

wp_enqueue_script('wptuts_myjs');
wp_localize_script(
	'wptuts_myjs',
	'wptuts_ajax',
	array(
		'url' => admin_url( 'admin-ajax.php' ), // URL to WordPress ajax handling page
		'nonce' => wp_create_nonce('my_nonce_action')
	)
);

Then inside our ‘wptuts_myjs’ script:

$.ajax({
	url: wptuts_ajax.url,
	dataType: 'json',
	data: {
		action: 'my_ajax_action',
		_ajax_nonce: wptuts_ajax.nonce,
	},
	...
});

Finally, inside your AJAX callback:

	check_ajax_referer('my_nonce_action');

Using More Than One Nonce

Normally one nonce per form (or per request) is sufficient. However, with WordPress the context is slightly complicated. For instance when you update a post, WordPress will perform permission and nonce checks – so presumably you don’t need to check a nonce for your function hooked onto save_post that deals with your custom meta box? Not so: save_post can be triggered in other instances, in various contexts or event manually – in fact whenever wp_update_post is called in fact. To be sure that the data you are receiving has come from your metabox you should use your own nonce.</p
>

Of course if you are using more than one nonce in a form it’s important that you give your nonce a unique name – that is a unique name for the hidden field containing the nonce value. If multiple nonces in a form have the same name, one will over-ride the other and somewhere a check that should pass, fails.

So when creating a nonce for your metabox, make sure you give it a unique name:

function my_metabox_callback($post) {
	$name='my_nonce_name'; // Make sure this is unique, prefix it with your plug-in/theme name
	$action='my_action_xyz_'.$post->ID; // This is the nonce action

	wp_nonce_field($action,$name);

	// Your meta box...
}

Then for your save_post meta-box:

add_action('save_post','my_metabox_save_post');
function my_metabox_save_post( $post_id ) {
	// Check its not an auto save
	if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE )
		return;

	// Check your data has been sent - this helps verify that we intend to process our metabox
	if ( !isset($_POST['my_nonce_name']) )
		return;

	// Check permissions
	if ( !current_user_can('edit_post', $post_id) )
		return;

	// Finally check the nonce
	check_admin_referer('my_action_xyz_'.$post_id, 'my_nonce_name');

	// Perform actions
}

If you are dealing with data from more than one metabox – you should, ideally, have a nonce for each one. Meta boxes can be removed, and if the meta box containing the nonce is removed, then not even valid requests will pass. Consequently any processing of other metaboxes that rely on that nonce will not occur.


Aesthetics

Now only privileged users can delete posts – and we have methods in place to prevent attackers tricking them into visiting the link. Currently, however, the link appears for everyone – we should tidy this up so that it only appears for users who are allowed to use it! I’ve left this step last because hiding the link from unprivileged users has no security benefit whatsoever. Obscurity is not security.

We have to assume that the attacker is able to completely inspect the source code (be it WordPress, theme or plug-in) – and if so, simply hiding the link alone does nothing: they can simply construct the URL themselves. Capability checks prevent this, because, even with the URL, they do not have the cookies that give them permission. Also, nonces prevent them from tricking you into visiting the URL by requiring a valid nonce.

So, as a completely aesthetic exercise, we include a capability check inside the wptuts_frontend_delete_link() function:

function wptuts_frontend_delete_link() {
	if ( current_user_can('delete_post', get_the_ID()) ) {
		$url = add_query_arg(
			array(
				'action' => 'wptuts_frontend_delete',
				'post' => get_the_ID();
			)
		);

		echo  "<a href='{$url}'>Delete</a>";
	}
}

Summary

It’s important to remember that capabilities indicate permission and nonces verify intention. Both are necessary to keep your plug-in secure, but one does not imply the other. Sometimes WordPress handles these checks for you – for instance when using the settings API. However, when hooking onto save_post it’s necessary to perform these checks.

Happy coding!

from Wptuts+ http://wp.tutsplus.com/tutorials/creative-coding/capabilities-and-nonces/

About these ads

Kommentar verfassen

Bitte logge dich mit einer dieser Methoden ein, um deinen Kommentar zu veröffentlichen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ photo

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s