Broken Access Control
Introduction
This article covers cases of possible Broken Access Control on WordPress. This includes improper hook/function/code usage inside 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 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 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 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) 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 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 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 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 for what process can be done from the REST API route callback
function.
Nonce leakage
In the case when an action is only protected by a nonce instead of a permission check, there is a chance that the nonce might be leaked to low-privileged users. It can lead to broken access control issues in such cases.
add_action('wp_enqueue_scripts', function() { wp_register_script('insecure-demo', false); wp_enqueue_script('insecure-demo'); wp_localize_script('insecure-demo', 'insecureDemo', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('delete_user_nonce'), ]);});
add_action('wp_ajax_nopriv_delete_user', function() { $nonce = $_POST['nonce'] ?? ''; $user_id = $_POST['user_id'] ?? 0;
if (!wp_verify_nonce($nonce, 'delete_user_nonce')) { wp_die('Invalid nonce'); }
if (wp_delete_user($user_id)) { echo "User $user_id deleted"; } else { echo "Failed to delete user"; }
wp_die();});
To exploit this, an unauthenticated user can grab the nonce from the WP site homepage source and perform a POST request to the /admin-ajax.php
endpoint with the nonce parameter to trigger the wp_delete_user
function.
curl <WORDPRESS_BASE_URL>/wp-admin/admin-ajax.php?action=delete_user -d "nonce=nonce_from_page_source&user_id=1"
Contributors

