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.
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.
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.
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.
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.
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
