Keeping your WordPress theme or plugin code secure is important to prevent possible attacks from malicious users.
We've previously covered how to sanitize, escape and validate form data in WordPress, as well as improving your WordPress theme quality with the VIP Scanner.
Today we'll look at how nonces (number used once) can also help keep your WordPress themes and plugins secure.
What Are WordPress Nonces?WordPress nonces are defined as:
… a "number used once" to help protect URLs and forms from certain types of misuse, malicious or otherwise.https://codex.wordpress.org/WordPress_Nonces
Whilst in WordPress a nonce isn't technically a number (it's a hash made up of letters and numbers), it does help prevent actions from being run by malicious users.
WordPress nonces work in two parts:
For example, when you go to delete a post in the WordPress Administration Screen, you'll notice the URL contains a _wpnonce parameter:http://127.0.0.1/tuts/wp-admin/post.php?post=542&action=trash&_wpnonce=a03ac85772
The routine, which deletes a post, will check that post 542 has a nonce value of a03ac85772 before deleting the post. If the nonce does not exist, or does not match the expected value, the post will not be deleted.
This prevents a malicious user potentially deleting lots of posts. For example, the following wouldn't work, as the nonce belongs to post ID 542:http://127.0.0.1/tuts/wp-admin/post.php?post=642&action=trash&_wpnonce=a03ac85772http://127.0.0.1/tuts/wp-admin/post.php?post=742&action=trash&_wpnonce=a03ac85772http://127.0.0.1/tuts/wp-admin/post.php?post=842&action=trash&_wpnonce=a03ac85772
Let's now look at how we can implement WordPress nonces in a plugin
Setting up Our WordPress PluginLet's start with a basic plugin that has its own settings screen. The settings screen has a field, which can be submitted and saved in the WordPress options table.
Enter the below code in a new file at wp-content/plugins/implementing-wordpress-nonces/implementing-wordpress-nonces.php:
< ?php /** * Plugin Name: Implementing WordPress Nonces * Plugin URI: http://www.sitepoint.com * Version: 1.0 * Author: n7 Studios * Author URI: http://www.n7studios.co.uk * Description: Example Plugin demonstrating how to implement WordPress Nonces * License: GPL2 */ class ImplementingWPNonces { function __construct() { // Plugin Details $this->plugin = new stdClass; $this->plugin->name = 'implementing-wordpress-nonces'; // Plugin Folder $this->plugin->displayName = 'Nonces'; // Plugin Name add_action( 'admin_menu', array( &$this, 'admin_menu' ) ); } /** * Register the plugin settings panel * * @since 1.0.0 */ function admin_menu() { add_menu_page( $this->plugin->displayName, $this->plugin->displayName, 'manage_options', $this->plugin->name, array( &$this, 'admin_screen' ), 'dashicons-admin-network' ); } /** * Output the Administration Screens * Save POSTed data from the Administration Panel into a WordPress option * * @since 1.0.0 */ function admin_screen() { // Save Settings if ( isset( $_REQUEST['submit'] ) ) { update_option( 'implementing_wordpress_nonces', sanitize_text_field( $_REQUEST[ 'implementing_wordpress_nonces' ] ) ); $message = __( 'Settings saved', $this->plugin->name ); } // Output form ?> < ?php echo $this->plugin->displayName; ?> < ?php if ( isset( $message ) ) { ?> < ?php } ?> < ?php } } $implementing_wordpress_nonces = new ImplementingWPNonces;Activate the plugin via the WordPress Administration > Plugins screen, and you'll see a new Nonces menu item displayed:
Click this, and you'll be taken to the settings screen with a single field:
Enter any value, click Save, and if everything worked, you'll see confirmation, along with the value you've just entered:
Demonstrating the Security FlawEnter the following URL into your web browser's address bar (replacing the domain with where you have WordPress installed):http://127.0.0.1/tuts/wp-admin/admin.php?page=implementing-wordpress-nonces&implementing_wordpress_nonces=abc
Notice what happened? The value was saved as abc, simply by directly accessing a URL and being logged into WordPress:
Whilst we could use $_POST instead of $_REQUEST in our code (we've used $_REQUEST to make it easier to demonstrate the security issue), this wouldn't help – a malicious user could still, either by themselves or by tricking you into clicking a link – get you to send a POST request to this screen, resulting in the option value being changed.
This is known as a Cross-Site Request Forgery (or CSRF). It's where a malicious web site, email, application, etc. causes the user's web browser to perform an unwanted action.
We'll now create and verify a WordPress nonce, to prevent this attack from being possible.
Securing Our Plugin with a NonceAs mentioned earlier, there are two steps to the process: first, we need to create a nonce that will be submitted with our form. Second, we need to verify that nonce when the form is submitted.
To create a nonce field in our form, we can use wp_nonce_field():
Retrieves or displays the nonce hidden form field… used to validate that the contents of the form request came from the current site and not somewhere else…
Add the following code just above our input button:
wp_nonce_field( 'implementing_wordpress_nonces_save', 'implementing_wordpress_nonces_nonce' );wp_nonce_field accepts four arguments – the first two are most important:
If we reload the settings screen, change our value and click Save, you'll notice the value still changes. We now need to implement a check for the nonce field that was submitted, using wp_verify_nonce( $name, $action ):
Verify that a nonce is correct and unexpired with the respect to a specified action. The function is used to verify the nonce sent in the current request usually accessed by the $_REQUEST PHP variable.
Replace the Save Settings section of our plugin's admin_screen() function with code the below:
// Save Settings if ( isset( $_REQUEST['implementing_wordpress_nonces'] ) ) { if ( isset( $_REQUEST[ 'implementing_wordpress_nonces_nonce' ] ) && wp_verify_nonce( $_REQUEST[ 'implementing_wordpress_nonces_nonce' ], 'implementing_wordpress_nonces_save' ) ) { update_option( 'implementing_wordpress_nonces', sanitize_text_field( $_REQUEST[ 'implementing_wordpress_nonces' ] ) ); $message = __( 'Settings saved', $this->plugin->name ); } else { // Nonce could not be verified - bail wp_die( __( 'Invalid nonce specified', $this->plugin->name ), __( 'Error', $this->plugin->name ), array( 'response' => 403, 'back_link' => 'admin.php?page=' . $this->plugin->name, ) ); } }This code performs a few actions:
To ensure our nonces are being created and validated, let's try to access our 'malicious' direct URL again:http://127.0.0.1/tuts/wp-admin/admin.php?page=implementing-wordpress-nonces&implementing_wordpress_nonces=abc.
If our nonces are implemented and being verified, you'll see the Invalid nonce specified notice:
Using Nonces in AJAX RequestsSay we want to save changes via an AJAX call, rather than reload the entire screen. We can do this with a few code tweaks.
First, let's load some JavaScript into our plugin, and register an AJAX handler, by adding the following to our __construct():
add_action( 'admin_enqueue_scripts', array( &$this, 'admin_scripts_css' ) ); add_action( 'wp_ajax_implementing_wp_nonces', array( &$this, 'admin_ajax_save' ) );In our class, add the corresponding admin_scripts_css function:
/** * Register and enqueue any JS and CSS for the WordPress Administration * * @since 1.0.0 */ function admin_scripts_css() { // JS wp_enqueue_script( $this->plugin->name, plugin_dir_url( __FILE__ ) . 'admin.js', array( 'jquery' ), '1.0', true ); }For our AJAX call, add the corresponding admin_ajax_save function:
/** * Saves POSTed settings data * * @since 1.0.0 */ function admin_ajax_save() { // Save option and return 1 update_option( 'implementing_wordpress_nonces', sanitize_text_field( $_REQUEST[ 'implementing_wordpress_nonces' ] ) ); echo 1; die(); }Finally, we need to create a new file in our plugin folder for our JavaScript routine, which will POST the data via AJAX when the form is submitted. Let's do this by creating a new file called admin.js, inserting the following:
jQuery( document ).ready( function( $ ) { $( 'form#implementing-wordpress-nonces' ).submit( function( e ) { // Prevent form submission e.preventDefault(); // Submit form via AJAX $.post( ajaxurl, // Set by WordPress { 'action': 'implementing_wp_nonces', 'implementing_wordpress_nonces': $( 'input#implementing_wordpress_nonces' ).val() }, function(response) { if ( response ) { alert( 'Settings Saved' ); } } ); }); } );Reload our settings screen, and if everything worked, when you submit the form, you'll get an on screen confirmation to say that the settings saved.
We can reload the settings screen again to make sure the value did update successfully:
Again, we now need to implement both the creation and validation of a WordPress nonce, to ensure that CSRF attacks can't happen via an AJAX request.
For creating nonces for AJAX requests, we can use the WordPress wp_create_nonce function:
Generates and returns a nonce. The nonce is generated based on the current time, the $action argument, and the current user ID.
It accepts a single $action argument, so we'd use:
$nonce = wp_create_nonce( 'implementing_wordpress_nonces_ajax_save' );To get this nonce hash into JavaScript's scope, we can use wp_localize_script() to send a JS object. Let's add the following code below the wp_enqueue_script call in our plugin:
wp_localize_script( $this->plugin->name, 'implementing_wordpress_nonces', array( 'nonce' => wp_create_nonce( 'implementing_wordpress_nonces_ajax_save' ), ) );We also need to update our JavaScript file to send this nonce value with the AJAX POST request:
jQuery( document ).ready( function( $ ) { $( 'form#implementing-wordpress-nonces' ).submit( function( e ) { // Prevent form submission e.preventDefault(); // Submit form via AJAX $.post( ajaxurl, // Set by WordPress { 'action': 'implementing_wp_nonces', 'nonce': implementing_wordpress_nonces.nonce, 'implementing_wordpress_nonces': $( 'input#implementing_wordpress_nonces' ).val() }, function(response) { if ( response == 1 ) { alert( 'Settings Saved' ); } else { alert( 'Invalid nonce specified' ); } } ); }); } );The JavaScript file now sends a $_POST variable called nonce with the value of wp_create_nonce.
Finally, we need to verify the nonce when it's sent in the AJAX request. Update our admin_ajax_save function:
/** * Saves POSTed settings data * * @since 1.0.0 */ function admin_ajax_save() { // Run a security check first. check_ajax_referer( 'implementing_wordpress_nonces_ajax_save', 'nonce' ); // Save option and return 1 update_option( 'implementing_wordpress_nonces', sanitize_text_field( $_REQUEST[ 'implementing_wordpress_nonces' ] ) ); echo 1; die(); }This uses check_ajax_referer, which checks if the given $action (implementing_wordpress_nonces_ajax_save) exists for the given POST field $name (nonce).
If it fails, -1 will be returned, triggering our JavaScript file to show an alert:
If it succeeds, 1 will be returned, triggering our JavaScript file to show a success message:
ConclusionWe've learnt what a WordPress nonce is, and how WordPress uses nonces to prevent malicious CSRF attacks. By building a basic WordPress plugin, we've shown how a plugin – and WordPress – can potentially be exploited if nonces are not created and verified.
Using wp_nonce_field() and wp_verify_nonce, we've shown how to validate nonces to prevent CSRF attacks. Finally, we implemented AJAX saving of POST data, and used wp_create_nonce and check_ajax_referer to ensure our AJAX requests are also as secure as possible.
For the full source code, check out the GitHub repository or download the code directly.
Source: What Are WordPress Nonces?
No comments:
Post a Comment