Securing an AJAX file-writing
November 20, 2013
In my earlier post I mentioned the FileWriter class: my actual implementation, the one that comes with the Adapter Classes plugin, I use the adclasses_Functions
and adclasses_Globals
adapter classes but the bulk and logic of it remains the same: it uses WordPress Filesystem API to write a file to disk.
As I've said in that earlier post the FileWriter
class comes with no security at all baked in it. The choice was made to be able to call it in any WordPress situation.
Where it's called
[caption id="attachment_312" align="aligncenter" width="1024"][](http://theaveragedev.local/wordpress/wp-content/uploads/2013/11/Screen-Shot-2013-11-20-at-20.08.41.png) Push the button to add drama[/caption] The screen above shows one use I make of the
FileWriter
class: clicking the button labeled Save functions to file triggers an AJAX action that dumps the current list of user defined functions to file. The file will be used to build the façade for the Functions
adapter class in tests.
Watch the button
On the admin page dedicated to the plugin settings I enqueue the following jQuery code, please note that the ajaxurl
variable is conveniently already localized by WordPress
/**
* Handles the click of the 'save functions' button in the Adapter Classes settings page.
* @return {none} Appends content to the #saving_results label.
*/
jQuery( document ).ready(function() {
jQuery('#save_functions').click(function() {
// get the nonce value from the nonce field on the page
nonce = jQuery('input#_wpnonce').val();
jQuery.ajax({
type:'POST',
url:ajaxurl,
data:{
action: 'adclasses_save_functions',
_ajax_nonce: nonce,
},
success: function (data, textStatus, XMLHttpRequest){
jQuery('#saving_results').html('');
jQuery('#saving_results').append(data);
},
error: function(data, textStatus, errorThrown){
alert(errorThrown);
}
});
});
});
which is pretty basic and will call a fucntion that will:
* get a list of the currently defined functions
* try to write the said functions to file via the FileWriter
all this passing along, to the function, the nonce
value generated using wp_nonce_field
PHP instruction.
Laying out the security
Since the damage that can be made allowing a file writing to be made via a JavaScript method is huge I have to take some precautions and prevent such cases using the WordPress nonce (number used once) mechanism.
In my code I use the function wp_nonce_field
while generating the output
<?php wp_nonce_field('adclasses_plugin_options'); ?>
and it will actually generate two hidden input fields like
<input type="hidden" id="_wpnonce" name="_wpnonce" value="a7bdce7682">
<input type="hidden" name="_wp_http_referer" value="/wp-admin/options-general.php?page=adclasses">
You know who sent me over to do you know what
If you imagine the AJAX request generated by the button click as a guy skulking in dark WordPress alleys and then knocking on WordPress AJAX handling system then the two generated inputs will be the you know who (_wp_http_referer
) and the password (_wpnonce
) catch phrases and the metaphore works well in keeping in mind that:
* <code>_wp_http_referer</code> will not change over time, no matter how many times you call the function the *you know who* will always be the same
* <code>_wpnonce</code> will change on each page refresh and this is good because it's the secret password
Just pass the door
On the other side of the imaginary door sits the WordPress AJAX system that will answer the AJAX request knocking and wil ask whom the caller is searching for; in coding terms this means that the function that's hooked to receive AJAX requests (the save_functions
function pasted below) will get to the door and ask from whom the calls comes from and which is the password. If the caller answers the security question right then the function acts and, finally and eventually, writes the file.
/**
* Saves a list of the defined user functions to file
* @return none
*/
public function save_functions()
{
// the url to get back to
$url = 'options-general.php?page=adclasses';
// the nonce to check
$nonce_action = 'adclasses_plugin_options';
// verify the nonce
if ( ! check_ajax_referer('adclasses_plugin_options', '_ajax_nonce', false)){
die ('There was an error verifying the call source.');
}
// prepare the file contents
$defined_functions = get_defined_functions();
$user_functions = $defined_functions['user'];
$contents = implode(',', $user_functions);
// the file to save the functions list to
$filename = ADCLASSES_ROOT . 'assets/functions.txt';
// the actual writing attempts
try {
$result = $this->file_writer->put_contents($url, $contents, $filename);
if (!$result) {
die(esc_html__('Permission to write is requested to save the functions list.', 'adclasses'));
}
// prepare the success message
$filename_markup = '<code>' . $filename . '</code>';
$message = sprintf(esc_html__('Functions saved to file in %1$s', 'adclasses') , $filename_markup);
die($message);
}
catch(Excpetions $e) {
if ($e instanceof RuntimeException) {
die(esc_html__('The functions could not be saved to file.', 'adclasses'));
}
else {
die(esc_html__('There was an error writing the functions to file.', 'adclasses'));
}
}
}